simetri.geometry.geometry

Geometry related utilities. These functions are used to perform geometric operations. They are not documented. Some of the are one off functions that are not used in the main codebase or tested.

   1"""Geometry related utilities.
   2These functions are used to perform geometric operations.
   3They are not documented. Some of the are one off functions that
   4are not used in the main codebase or tested."""
   5
   6# To do: Clean up this module and add documentation.
   7
   8from __future__ import annotations
   9
  10from math import hypot, atan2, floor, pi, sin, cos, sqrt, exp, sqrt, acos
  11from itertools import cycle
  12from typing import Any, Union, Sequence
  13import re
  14
  15
  16import numpy as np
  17from numpy import isclose, array, around
  18
  19from simetri.helpers.utilities import (
  20    flatten,
  21    lerp,
  22    sanitize_graph_edges,
  23    equal_cycles,
  24    reg_poly_points,
  25)
  26
  27from ..helpers.vector import Vector2D
  28from ..graphics.common import (
  29    get_defaults,
  30    common_properties,
  31    Point,
  32    Line,
  33    Sequence,
  34    i_vec,
  35    j_vec,
  36    VecType,
  37    axis_x,
  38    axis_y,
  39)
  40from ..graphics.all_enums import Connection, Types
  41from ..settings.settings import defaults
  42
  43array = np.array
  44around = np.around
  45
  46TWO_PI = 2 * pi  # 360 degrees
  47
  48
  49def is_number(x: Any) -> bool:
  50    """
  51    Return True if x is a number.
  52
  53    Args:
  54        x (Any): The input value to check.
  55
  56    Returns:
  57        bool: True if x is a number, False otherwise.
  58    """
  59    return isinstance(x, (int, float, complex)) and not isinstance(x, bool)
  60
  61
  62def bbox_overlap(
  63    min_x1: float,
  64    min_y1: float,
  65    max_x2: float,
  66    max_y2: float,
  67    min_x3: float,
  68    min_y3: float,
  69    max_x4: float,
  70    max_y4: float,
  71) -> bool:
  72    """
  73    Given two bounding boxes, return True if they overlap.
  74
  75    Args:
  76        min_x1 (float): Minimum x-coordinate of the first bounding box.
  77        min_y1 (float): Minimum y-coordinate of the first bounding box.
  78        max_x2 (float): Maximum x-coordinate of the first bounding box.
  79        max_y2 (float): Maximum y-coordinate of the first bounding box.
  80        min_x3 (float): Minimum x-coordinate of the second bounding box.
  81        min_y3 (float): Minimum y-coordinate of the second bounding box.
  82        max_x4 (float): Maximum x-coordinate of the second bounding box.
  83        max_y4 (float): Maximum y-coordinate of the second bounding box.
  84
  85    Returns:
  86        bool: True if the bounding boxes overlap, False otherwise.
  87    """
  88    return not (
  89        max_x2 < min_x3 or max_x4 < min_x1 or max_y2 < min_y3 or max_y4 < min_y1
  90    )
  91
  92
  93def sine_wave(
  94    amplitude: float,
  95    frequency: float,
  96    duration: float,
  97    sample_rate: float,
  98    phase: float = 0,
  99) -> 'ndarray':
 100    """
 101    Generate a sine wave.
 102
 103    Args:
 104        amplitude (float): Amplitude of the wave.
 105        frequency (float): Frequency of the wave.
 106        duration (float): Duration of the wave.
 107        sample_rate (float): Sample rate.
 108        phase (float, optional): Phase angle of the wave. Defaults to 0.
 109
 110    Returns:
 111        np.ndarray: Time and signal arrays representing the sine wave.
 112    """
 113    time = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
 114    signal = amplitude * np.sin(2 * np.pi * frequency * time + phase)
 115    # plt.plot(time, signal)
 116    # plt.xlabel('Time (s)')
 117    # plt.ylabel('Amplitude')
 118    # plt.title('Discretized Sine Wave')
 119    # plt.grid(True)
 120    # plt.show()
 121    return time, signal
 122
 123
 124def damping_function(amplitude, duration, sample_rate):
 125    """
 126    Generates a damping function based on the given amplitude, duration, and sample rate.
 127
 128    Args:
 129        amplitude (float): The initial amplitude of the damping function.
 130        duration (float): The duration over which the damping occurs, in seconds.
 131        sample_rate (float): The number of samples per second.
 132
 133    Returns:
 134        list: A list of float values representing the damping function over time.
 135    """
 136    damping = []
 137    for i in range(int(duration * sample_rate)):
 138        damping.append(amplitude * exp(-i / (duration * sample_rate)))
 139    return damping
 140
 141def sine_points(
 142    period: float = 40,
 143    amplitude: float = 20,
 144    duration: float = 40,
 145    n_points: int = 100,
 146    phase_angle: float = 0,
 147    damping: float = 0,
 148) -> 'ndarray':
 149    """
 150    Generate sine wave points.
 151
 152    Args:
 153        amplitude (float): Amplitude of the wave.
 154        frequency (float): Frequency of the wave.
 155        duration (float): Duration of the wave.
 156        sample_rate (float): Sample rate.
 157        phase (float, optional): Phase angle of the wave. Defaults to 0.
 158        damping (float, optional): Damping coefficient. Defaults to 0.
 159    Returns:
 160        np.ndarray: Array of points representing the sine wave.
 161    """
 162    phase = phase_angle
 163    freq = 1 / period
 164    n_cycles = duration / period
 165    x = np.linspace(0, duration, int(n_points * n_cycles))
 166    y = amplitude * np.sin(2 * np.pi * freq * x + phase)
 167    if damping:
 168        y *= np.exp(-damping * x)
 169    vertices = np.column_stack((x, y)).tolist()
 170
 171    return vertices
 172
 173
 174def check_consecutive_duplicates(points, rtol=0, atol=None) -> bool:
 175    """Check for consecutive duplicate points in a list of points.
 176
 177        Args:
 178            points (list): List of points to check.
 179            rtol (float, optional): Relative tolerance. Defaults to 0.
 180            atol (float, optional): Absolute tolerance. Defaults to None.
 181
 182        Returns:
 183            bool: True if consecutive duplicate points are found, False otherwise.
 184    """
 185    if atol is None:
 186        atol = defaults["atol"]
 187    if isinstance(points, np.ndarray):
 188        points = points.tolist()
 189    if points and len(points) > 1:
 190        for i, pnt in enumerate(points[:-1]):
 191            next_pnt = points[i+1]
 192            val1 = pnt[0] + pnt[1]
 193            val2 = next_pnt[0] + next_pnt[1]
 194            if isclose(val1, val2, rtol=0, atol=atol):
 195                if np.allclose(pnt, next_pnt, rtol=0, atol=atol):
 196                    return True
 197
 198    return False
 199
 200def circle_inversion(point, center, radius):
 201    """
 202    Inverts a point with respect to a circle.
 203
 204    Args:
 205        point (tuple): The point to invert, represented as a tuple (x, y).
 206        center (tuple): The center of the circle, represented as a tuple (x, y).
 207        radius (float): The radius of the circle.
 208
 209    Returns:
 210        tuple: The inverted point, represented as a tuple (x, y).
 211    """
 212    x, y = point[:2]
 213    cx, cy = center[:2]
 214    # Calculate the distance from the point to the center of the circle
 215    dist = sqrt((x - cx) ** 2 + (y - cy) ** 2)
 216    # If the point is at the center of the circle, return the point at infinity
 217    if dist == 0:
 218        return float("inf"), float("inf")
 219    # Calculate the distance from the inverted point to the center of the circle
 220    inv_dist = radius**2 / dist
 221    # Calculate the inverted point
 222    inv_x = cx + inv_dist * (x - cx) / dist
 223    inv_y = cy + inv_dist * (y - cy) / dist
 224    return inv_x, inv_y
 225
 226
 227def line_segment_bbox(
 228    x1: float, y1: float, x2: float, y2: float
 229) -> tuple[float, float, float, float]:
 230    """
 231    Return the bounding box of a line segment.
 232
 233    Args:
 234        x1 (float): Segment start point x-coordinate.
 235        y1 (float): Segment start point y-coordinate.
 236        x2 (float): Segment end point x-coordinate.
 237        y2 (float): Segment end point y-coordinate.
 238
 239    Returns:
 240        tuple: Bounding box as (min_x, min_y, max_x, max_y).
 241    """
 242    return (min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2))
 243
 244
 245def line_segment_bbox_check(seg1: Line, seg2: Line) -> bool:
 246    """
 247    Given two line segments, return True if their bounding boxes overlap.
 248
 249    Args:
 250        seg1 (Line): First line segment.
 251        seg2 (Line): Second line segment.
 252
 253    Returns:
 254        bool: True if the bounding boxes overlap, False otherwise.
 255    """
 256    x1, y1 = seg1[0]
 257    x2, y2 = seg1[1]
 258    x3, y3 = seg2[0]
 259    x4, y4 = seg2[1]
 260    return bbox_overlap(
 261        *line_segment_bbox(x1, y1, x2, y2), *line_segment_bbox(x3, y3, x4, y4)
 262    )
 263
 264
 265def all_close_points(
 266    points: Sequence[Sequence], dist_tol: float = None, with_dist: bool = False
 267) -> dict[int, list[tuple[Point, int]]]:
 268    """
 269    Find all close points in a list of points along with their ids.
 270
 271    Args:
 272        points (Sequence[Sequence]): List of points with ids [[x1, y1, id1], [x2, y2, id2], ...].
 273        dist_tol (float, optional): Distance tolerance. Defaults to None.
 274        with_dist (bool, optional): Whether to include distances in the result. Defaults to False.
 275
 276    Returns:
 277        dict: Dictionary of the form {id1: [id2, id3, ...], ...}.
 278    """
 279    if dist_tol is None:
 280        dist_tol = defaults["dist_tol"]
 281    point_arr = np.array(points, dtype=np.float32)  # points array [[x1, y1, id1], ...]]
 282    n_rows = len(points)
 283    point_arr = point_arr[point_arr[:, 0].argsort()]  # sort by x values in the
 284    # first column
 285    xmin = point_arr[:, 0] - dist_tol * 2
 286    xmin = xmin.reshape(n_rows, 1)
 287    xmax = point_arr[:, 0] + dist_tol * 2
 288    xmax = xmax.reshape(n_rows, 1)
 289    point_arr = np.concatenate((point_arr, xmin, xmax), 1)  # [x, y, id, xmin, xmax]
 290
 291    i_id, i_xmin, i_xmax = 2, 3, 4  # column indices
 292    d_connections = {}
 293    for i in range(n_rows):
 294        d_connections[int(point_arr[i, 2])] = []
 295    pairs = []
 296    dist_tol2 = dist_tol * dist_tol
 297    for i in range(n_rows):
 298        x, y, id1, sl_xmin, sl_xmax = point_arr[i, :]
 299        id1 = int(id1)
 300        point = (x, y)
 301        start = i + 1
 302        candidates = point_arr[start:, :][
 303            (
 304                (point_arr[start:, i_xmax] >= sl_xmin)
 305                & (point_arr[start:, i_xmin] <= sl_xmax)
 306            )
 307        ]
 308        for cand in candidates:
 309            id2 = int(cand[i_id])
 310            point2 = cand[:2]
 311            if close_points2(point, point2, dist2=dist_tol2):
 312                d_connections[id1].append(id2)
 313                d_connections[id2].append(id1)
 314                if with_dist:
 315                    pairs.append((id1, id2, distance(point, point2)))
 316                else:
 317                    pairs.append((id1, id2))
 318    res = {}
 319    for k, v in d_connections.items():
 320        if v:
 321            res[k] = v
 322    return res, pairs
 323
 324
 325def is_simple(
 326    polygon,
 327    rtol: float = None,
 328    atol: float = None,
 329) -> bool:
 330    """
 331    Return True if the polygon is simple.
 332
 333    Args:
 334        polygon (list): List of points representing the polygon.
 335        rtol (float, optional): Relative tolerance. Defaults to None.
 336        atol (float, optional): Absolute tolerance. Defaults to None.
 337
 338    Returns:
 339        bool: True if the polygon is simple, False otherwise.
 340    """
 341    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
 342
 343    if not close_points2(polygon[0], polygon[-1]):
 344        polygon.append(polygon[0])
 345    segments = [[polygon[i], polygon[i + 1]] for i in range(len(polygon) - 1)]
 346
 347    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
 348    segment_coords = []
 349    for segment in segments:
 350        segment_coords.append(
 351            [segment[0][0], segment[0][1], segment[1][0], segment[1][1]]
 352        )
 353    seg_arr = np.array(segment_coords)  # segments array
 354    n_rows = seg_arr.shape[0]
 355    xmin = np.minimum(seg_arr[:, 0], seg_arr[:, 2]).reshape(n_rows, 1)
 356    xmax = np.maximum(seg_arr[:, 0], seg_arr[:, 2]).reshape(n_rows, 1)
 357    ymin = np.minimum(seg_arr[:, 1], np.maximum(seg_arr[:, 3])).reshape(n_rows, 1)
 358    ymax = np.maximum(seg_arr[:, 1], seg_arr[:, 3]).reshape(n_rows, 1)
 359    id_ = np.arange(n_rows).reshape(n_rows, 1)
 360    seg_arr = np.concatenate((seg_arr, xmin, ymin, xmax, ymax, id_), 1)
 361    seg_arr = seg_arr[seg_arr[:, 4].argsort()]
 362    i_xmin, i_ymin, i_xmax, i_ymax, i_id = range(4, 9)  # column indices
 363
 364    s_processed = set()  # set of processed segment pairs
 365    for i in range(n_rows):
 366        x1, y1, x2, y2, sl_xmin, sl_ymin, sl_xmax, sl_ymax, id1 = seg_arr[i, :]
 367        id1 = int(id1)
 368        segment = [x1, y1, x2, y2]
 369        start = i + 1  # keep pushing the sweep line forward
 370        candidates = seg_arr[start:, :][
 371            (
 372                (
 373                    (seg_arr[start:, i_xmax] >= sl_xmin)
 374                    & (seg_arr[start:, i_xmin] <= sl_xmax)
 375                )
 376                & (
 377                    (seg_arr[start:, i_ymax] >= sl_ymin)
 378                    & (seg_arr[start:, i_ymin] <= sl_ymax)
 379                )
 380            )
 381        ]
 382        for cand in candidates:
 383            id2 = int(cand[i_id])
 384            pair = frozenset((id1, id2))
 385            if pair in s_processed:
 386                continue
 387            s_processed.add(pair)
 388            seg2 = cand[:4]
 389            x1, y1, x2, y2 = segment
 390            x3, y3, x4, y4 = seg2
 391            res = intersection3(x1, y1, x2, y2, x3, y3, x4, y4)
 392            if res[0] == Connection.COLL_CHAIN:
 393                length1 = distance((x1, y1), (x2, y2))
 394                length2 = distance((x3, y3), (x4, y4))
 395                p1, p2 = res[1][0], res[1][2]
 396                chain_length = distance(p1, p2)
 397                if not isclose(length1 + length2, chain_length, rtol=rtol, atol=atol):
 398                    return False
 399                else:
 400                    continue
 401            if res[0] in (Connection.CHAIN, Connection.PARALLEL):
 402                continue
 403            if res[0] != Connection.DISJOINT:
 404                return False
 405
 406    return True
 407
 408
 409def all_intersections(
 410    segments: Sequence[Line],
 411    rtol: float = None,
 412    atol: float = None,
 413    use_intersection3: bool = False,
 414) -> dict[int, list[tuple[Point, int]]]:
 415    """
 416    Find all intersection points of the given list of segments
 417    (sweep line algorithm variant)
 418
 419    Args:
 420        segments (Sequence[Line]): List of line segments [[[x1, y1], [x2, y2]], [[x1, y1], [x2, y2]], ...].
 421        rtol (float, optional): Relative tolerance. Defaults to None.
 422        atol (float, optional): Absolute tolerance. Defaults to None.
 423        use_intersection3 (bool, optional): Whether to use intersection3 function. Defaults to False.
 424
 425    Returns:
 426        dict: Dictionary of the form {segment_id: [[id1, (x1, y1)], [id2, (x2, y2)]], ...}.
 427    """
 428    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
 429    segment_coords = []
 430    for segment in segments:
 431        segment_coords.append(
 432            [segment[0][0], segment[0][1], segment[1][0], segment[1][1]]
 433        )
 434    seg_arr = np.array(segment_coords)  # segments array
 435    n_rows = seg_arr.shape[0]
 436    xmin = np.minimum(seg_arr[:, 0], seg_arr[:, 2]).reshape(n_rows, 1)
 437    xmax = np.maximum(seg_arr[:, 0], seg_arr[:, 2]).reshape(n_rows, 1)
 438    ymin = np.minimum(seg_arr[:, 1], seg_arr[:, 3]).reshape(n_rows, 1)
 439    ymax = np.maximum(seg_arr[:, 1], seg_arr[:, 3]).reshape(n_rows, 1)
 440    id_ = np.arange(n_rows).reshape(n_rows, 1)
 441    seg_arr = np.concatenate((seg_arr, xmin, ymin, xmax, ymax, id_), 1)
 442    seg_arr = seg_arr[seg_arr[:, 4].argsort()]
 443    i_xmin, i_ymin, i_xmax, i_ymax, i_id = range(4, 9)  # column indices
 444    # ind1, ind2 are indexes of segments in the list of segments
 445    d_ind1_x_point_ind2 = {}  # {id1: [((x, y), id2), ...], ...}
 446    d_ind1_conn_type_x_res_ind2 = {}  # {id1: [(conn_type, x_res, id2), ...], ...}
 447    for i in range(n_rows):
 448        if use_intersection3:
 449            d_ind1_conn_type_x_res_ind2[i] = []
 450        else:
 451            d_ind1_x_point_ind2[i] = []
 452    x_points = []  # intersection points
 453    s_processed = set()  # set of processed segment pairs
 454    for i in range(n_rows):
 455        x1, y1, x2, y2, sl_xmin, sl_ymin, sl_xmax, sl_ymax, id1 = seg_arr[i, :]
 456        id1 = int(id1)
 457        segment = [x1, y1, x2, y2]
 458        start = i + 1  # keep pushing the sweep line forward
 459        # filter by overlap of the bounding boxes of the segments with the
 460        # sweep line's active segment. If the bounding boxes do not overlap,
 461        # the segments cannot intersect. If the bounding boxes overlap,
 462        # the segments may intersect.
 463        candidates = seg_arr[start:, :][
 464            (
 465                (
 466                    (seg_arr[start:, i_xmax] >= sl_xmin)
 467                    & (seg_arr[start:, i_xmin] <= sl_xmax)
 468                )
 469                & (
 470                    (seg_arr[start:, i_ymax] >= sl_ymin)
 471                    & (seg_arr[start:, i_ymin] <= sl_ymax)
 472                )
 473            )
 474        ]
 475        for cand in candidates:
 476            id2 = int(cand[i_id])
 477            pair = frozenset((id1, id2))
 478            if pair in s_processed:
 479                continue
 480            s_processed.add(pair)
 481            seg2 = cand[:4]
 482            if use_intersection3:
 483                # connection type, point/segment
 484                res = intersection3(*segment, *seg2, rtol, atol)
 485                conn_type, x_res = res  # x_res can be a segment or a point
 486            else:
 487                # connection type, point
 488                res = intersection2(*segment, *seg2, rtol, atol)
 489                conn_type, x_point = res
 490            if use_intersection3:
 491                if conn_type not in [Connection.DISJOINT, Connection.PARALLEL]:
 492                    d_ind1_conn_type_x_res_ind2[id1].append((conn_type, x_res, id2))
 493                    d_ind1_conn_type_x_res_ind2[id2].append((conn_type, x_res, id1))
 494            else:
 495                if conn_type == Connection.INTERSECT:
 496                    d_ind1_x_point_ind2[id1].append((x_point, id2))
 497                    d_ind1_x_point_ind2[id2].append((x_point, id1))
 498                    x_points.append(res[1])
 499
 500    d_results = {}
 501    if use_intersection3:
 502        for k, v in d_ind1_conn_type_x_res_ind2.items():
 503            if v:
 504                d_results[k] = v
 505        res = d_results
 506    else:
 507        for k, v in d_ind1_x_point_ind2.items():
 508            if v:
 509                d_results[k] = v
 510        res = d_results, x_points
 511
 512    return res
 513
 514
 515def dot_product2(a: Point, b: Point, c: Point) -> float:
 516    """Dot product of two vectors. AB and BC
 517    Args:
 518        a (Point): First point, creating vector BA
 519        b (Point): Second point, common point for both vectors
 520        c (Point): Third point, creating vector BC
 521
 522    Returns:
 523        float: The dot product of vectors BA and BC
 524    Note:
 525        The function calculates (a-b)·(c-b) which is the dot product of vectors BA and BC.
 526        This is useful for finding angles between segments that share a common point.
 527    """
 528    a_x, a_y = a[:2]
 529    b_x, b_y = b[:2]
 530    c_x, c_y = c[:2]
 531    b_a_x = a_x - b_x
 532    b_a_y = a_y - b_y
 533    b_c_x = c_x - b_x
 534    b_c_y = c_y - b_y
 535    return b_a_x * b_c_x + b_a_y * b_c_y
 536
 537
 538def cross_product2(a: Point, b: Point, c: Point) -> float:
 539    """
 540    Return the cross product of two vectors: BA and BC.
 541
 542    Args:
 543        a (Point): First point, creating vector BA
 544        b (Point): Second point, common point for both vectors
 545        c (Point): Third point, creating vector BC
 546
 547    Returns:
 548        float: The z-component of cross product between vectors BA and BC
 549
 550    Note:
 551        This gives the signed area of the parallelogram formed by the vectors BA and BC.
 552        The sign indicates the orientation (positive for counter-clockwise, negative for clockwise).
 553        It is useful for determining the orientation of three points and calculating angles.
 554
 555    vec1 = b - a
 556    vec2 = c - b
 557    """
 558    a_x, a_y = a[:2]
 559    b_x, b_y = b[:2]
 560    c_x, c_y = c[:2]
 561    b_a_x = a_x - b_x
 562    b_a_y = a_y - b_y
 563    b_c_x = c_x - b_x
 564    b_c_y = c_y - b_y
 565    return b_a_x * b_c_y - b_a_y * b_c_x
 566
 567
 568def angle_between_lines2(point1: Point, point2: Point, point3: Point) -> float:
 569    """
 570    Given line1 as point1 and point2, and line2 as point2 and point3
 571    return the angle between two lines
 572    (point2 is the corner point)
 573
 574    Args:
 575        point1 (Point): First point of the first line.
 576        point2 (Point): Second point of the first line and first point of the second line.
 577        point3 (Point): Second point of the second line.
 578
 579    Returns:
 580        float: Angle between the two lines in radians.
 581    """
 582    return atan2(
 583        cross_product2(point1, point2, point3), dot_product2(point1, point2, point3)
 584    )
 585
 586
 587def angled_line(line: Line, theta: float) -> Line:
 588    """
 589    Given a line find another line with theta radians between them.
 590
 591    Args:
 592        line (Line): Input line.
 593        theta (float): Angle in radians.
 594
 595    Returns:
 596        Line: New line with the given angle.
 597    """
 598    # find the angle of the line
 599    x1, y1 = line[0]
 600    x2, y2 = line[1]
 601    theta1 = atan2(y2 - y1, x2 - x1)
 602    theta2 = theta1 + theta
 603    # find the length of the line
 604    dx = x2 - x1
 605    dy = y2 - y1
 606    length_ = (dx**2 + dy**2) ** 0.5
 607    # find the new line
 608    x3 = x1 + length_ * cos(theta2)
 609    y3 = y1 + length_ * sin(theta2)
 610
 611    return [(x1, y1), (x3, y3)]
 612
 613
 614def angled_vector(angle: float) -> Sequence[float]:
 615    """
 616    Return a vector with the given angle
 617
 618    Args:
 619        angle (float): Angle in radians.
 620
 621    Returns:
 622        Sequence[float]: Vector with the given angle.
 623    """
 624    return [cos(angle), sin(angle)]
 625
 626
 627def close_points2(p1: Point, p2: Point, dist2: float = 0.01) -> bool:
 628    """
 629    Return True if two points are close to each other.
 630
 631    Args:
 632        p1 (Point): First point.
 633        p2 (Point): Second point.
 634        dist2 (float, optional): Square of the threshold distance. Defaults to 0.01.
 635
 636    Returns:
 637        bool: True if the points are close to each other, False otherwise.
 638    """
 639    return distance2(p1, p2) <= dist2
 640
 641
 642def close_angles(angle1: float, angle2: float, angtol=None) -> bool:
 643    """
 644    Return True if two angles are close to each other.
 645
 646    Args:
 647        angle1 (float): First angle in radians.
 648        angle2 (float): Second angle in radians.
 649        angtol (float, optional): Angle tolerance. Defaults to None.
 650
 651    Returns:
 652        bool: True if the angles are close to each other, False otherwise.
 653    """
 654    if angtol is None:
 655        angtol = defaults["angtol"]
 656
 657    return (abs(angle1 - angle2) % (2 * pi)) < angtol
 658
 659
 660def distance(p1: Point, p2: Point) -> float:
 661    """
 662    Return the distance between two points.
 663
 664    Args:
 665        p1 (Point): First point.
 666        p2 (Point): Second point.
 667
 668    Returns:
 669        float: Distance between the two points.
 670    """
 671    return hypot(p2[0] - p1[0], p2[1] - p1[1])
 672
 673
 674def distance2(p1: Point, p2: Point) -> float:
 675    """
 676    Return the squared distance between two points.
 677    Useful for comparing distances without the need to
 678    compute the square root.
 679
 680    Args:
 681        p1 (Point): First point.
 682        p2 (Point): Second point.
 683
 684    Returns:
 685        float: Squared distance between the two points.
 686    """
 687    return (p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2
 688
 689
 690def connect2(
 691    poly_point1: list[Point],
 692    poly_point2: list[Point],
 693    dist_tol: float = None,
 694    rtol: float = None,
 695) -> list[Point]:
 696    """
 697    Connect two polypoints together.
 698
 699    Args:
 700        poly_point1 (list[Point]): First list of points.
 701        poly_point2 (list[Point]): Second list of points.
 702        dist_tol (float, optional): Distance tolerance. Defaults to None.
 703        rtol (float, optional): Relative tolerance. Defaults to None.
 704
 705    Returns:
 706        list[Point]: Connected list of points.
 707    """
 708    rtol, dist_tol = get_defaults(["rtol", "dist_tol"], [rtol, dist_tol])
 709    dist_tol2 = dist_tol * dist_tol
 710    start1, end1 = poly_point1[0], poly_point1[-1]
 711    start2, end2 = poly_point2[0], poly_point2[-1]
 712    pp1 = poly_point1[:]
 713    pp2 = poly_point2[:]
 714    points = []
 715    if close_points2(end1, start2, dist2=dist_tol2):
 716        points.extend(pp1)
 717        points.extend(pp2[1:])
 718    elif close_points2(end1, end2, dist2=dist_tol2):
 719        points.extend(pp1)
 720        pp2.reverse()
 721        points.extend(pp2[1:])
 722    elif close_points2(start1, start2, dist2=dist_tol2):
 723        pp1.reverse()
 724        points.extend(pp1)
 725        points.extend(pp2[1:])
 726    elif close_points2(start1, end2, dist2=dist_tol2):
 727        pp1.reverse()
 728        points.extend(pp1)
 729        pp2.reverse()
 730        points.extend(pp2[1:])
 731
 732    return points
 733
 734
 735def stitch(
 736    lines: list[Line],
 737    closed: bool = True,
 738    return_points: bool = True,
 739    rtol: float = None,
 740    atol: float = None,
 741) -> list[Point]:
 742    """
 743    Stitches a list of lines together.
 744
 745    Args:
 746        lines (list[Line]): List of lines to stitch.
 747        closed (bool, optional): Whether the lines form a closed shape. Defaults to True.
 748        return_points (bool, optional): Whether to return points or lines. Defaults to True.
 749        rtol (float, optional): Relative tolerance. Defaults to None.
 750        atol (float, optional): Absolute tolerance. Defaults to None.
 751
 752    Returns:
 753        list[Point]: Stitched list of points or lines.
 754    """
 755    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
 756    if closed:
 757        points = []
 758    else:
 759        points = [lines[0][0]]
 760    for i, line in enumerate(lines[:-1]):
 761        x1, y1 = line[0]
 762        x2, y2 = line[1]
 763        x3, y3 = lines[i + 1][0]
 764        x4, y4 = lines[i + 1][1]
 765        x_point = intersect2(x1, y1, x2, y2, x3, y3, x4, y4)
 766        if x_point:
 767            points.append(x_point)
 768    if closed:
 769        x1, y1 = lines[-1][0]
 770        x2, y2 = lines[-1][1]
 771        x3, y3 = lines[0][0]
 772        x4, y4 = lines[0][1]
 773        final_x = intersect2(
 774            x1,
 775            y1,
 776            x2,
 777            y2,
 778            x3,
 779            y3,
 780            x4,
 781            y4,
 782        )
 783        if final_x:
 784            points.insert(0, final_x)
 785            points.append(final_x)
 786    else:
 787        points.append(lines[-1][1])
 788    if return_points:
 789        res = points
 790    else:
 791        res = connected_pairs(points)
 792
 793    return res
 794
 795
 796def double_offset_polylines(
 797    lines: list[Point], offset: float = 1, rtol: float = None, atol: float = None
 798) -> list[Point]:
 799    """
 800    Return a list of double offset lines from a list of lines.
 801
 802    Args:
 803        lines (list[Point]): List of points representing the lines.
 804        offset (float, optional): Offset distance. Defaults to 1.
 805        rtol (float, optional): Relative tolerance. Defaults to None.
 806        atol (float, optional): Absolute tolerance. Defaults to None.
 807
 808    Returns:
 809        list[Point]: List of double offset lines.
 810    """
 811    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
 812    lines1 = []
 813    lines2 = []
 814    for i, point in enumerate(lines[:-1]):
 815        line = [point, lines[i + 1]]
 816        line1, line2 = double_offset_lines(line, offset)
 817        lines1.append(line1)
 818        lines2.append(line2)
 819    lines1 = stitch(lines1, closed=False)
 820    lines2 = stitch(lines2, closed=False)
 821    return [lines1, lines2]
 822
 823
 824def polygon_cg(points: list[Point]) -> Point:
 825    """
 826    Given a list of points that define a polygon, return the center point.
 827
 828    Args:
 829        points (list[Point]): List of points representing the polygon.
 830
 831    Returns:
 832        Point: Center point of the polygon.
 833    """
 834    cx = cy = 0
 835    n_points = len(points)
 836    for i in range(n_points):
 837        x = points[i][0]
 838        y = points[i][1]
 839        xnext = points[(i + 1) % n_points][0]
 840        ynext = points[(i + 1) % n_points][1]
 841
 842        temp = x * ynext - xnext * y
 843        cx += (x + xnext) * temp
 844        cy += (y + ynext) * temp
 845    area_ = polygon_area(points)
 846    denom = area_ * 6
 847    if denom:
 848        res = [cx / denom, cy / denom]
 849    else:
 850        res = None
 851    return res
 852
 853
 854def polygon_center2(polygon_points: list[Point]) -> Point:
 855    """
 856    Given a list of points that define a polygon, return the center point.
 857
 858    Args:
 859        polygon_points (list[Point]): List of points representing the polygon.
 860
 861    Returns:
 862        Point: Center point of the polygon.
 863    """
 864    n = len(polygon_points)
 865    x = 0
 866    y = 0
 867    for point in polygon_points:
 868        x += point[0]
 869        y += point[1]
 870    x = x / n
 871    y = y / n
 872    return [x, y]
 873
 874
 875def polygon_center(polygon_points: list[Point]) -> Point:
 876    """
 877    Given a list of points that define a polygon, return the center point.
 878
 879    Args:
 880        polygon_points (list[Point]): List of points representing the polygon.
 881
 882    Returns:
 883        Point: Center point of the polygon.
 884    """
 885    x = 0
 886    y = 0
 887    for i, point in enumerate(polygon_points[:-1]):
 888        x += point[0] * (polygon_points[i - 1][1] - polygon_points[i + 1][1])
 889        y += point[1] * (polygon_points[i - 1][0] - polygon_points[i + 1][0])
 890    area_ = polygon_area(polygon_points)
 891    return (x / (6 * area_), y / (6 * area_))
 892
 893
 894def offset_polygon(
 895    polygon: list[Point], offset: float = -1, dist_tol: float = None
 896) -> list[Point]:
 897    """
 898    Return a list of offset lines from a list of lines.
 899
 900    Args:
 901        polygon (list[Point]): List of points representing the polygon.
 902        offset (float, optional): Offset distance. Defaults to -1.
 903        dist_tol (float, optional): Distance tolerance. Defaults to None.
 904
 905    Returns:
 906        list[Point]: List of offset lines.
 907    """
 908    if dist_tol is None:
 909        dist_tol = defaults["dist_tol"]
 910    polygon = list(polygon[:])
 911    dist_tol2 = dist_tol * dist_tol
 912    if not right_handed(polygon):
 913        polygon.reverse()
 914    if not close_points2(polygon[0], polygon[-1], dist2=dist_tol2):
 915        polygon.append(polygon[0])
 916    poly = []
 917    for i, point in enumerate(polygon[:-1]):
 918        line = [point, polygon[i + 1]]
 919        offset_edge = offset_line(line, -offset)
 920        poly.append(offset_edge)
 921
 922    poly = stitch(poly, closed=True)
 923    return poly
 924
 925
 926def double_offset_polygons(
 927    polygon: list[Point], offset: float = 1, dist_tol: float = None, **kwargs
 928) -> list[Point]:
 929    """
 930    Return a list of double offset lines from a list of lines.
 931
 932    Args:
 933        polygon (list[Point]): List of points representing the polygon.
 934        offset (float, optional): Offset distance. Defaults to 1.
 935        dist_tol (float, optional): Distance tolerance. Defaults to None.
 936
 937    Returns:
 938        list[Point]: List of double offset lines.
 939    """
 940    if dist_tol is None:
 941        dist_tol = defaults["dist_tol"]
 942    if not right_handed(polygon):
 943        polygon.reverse()
 944    poly1 = []
 945    poly2 = []
 946    for i, point in enumerate(polygon[:-1]):
 947        line = [point, polygon[i + 1]]
 948        line1, line2 = double_offset_lines(line, offset)
 949        poly1.append(line1)
 950        poly2.append(line2)
 951    poly1 = stitch(poly1)
 952    poly2 = stitch(poly2)
 953    if "canvas" in kwargs:
 954        canvas = kwargs["canvas"]
 955        if canvas:
 956            canvas.new_page()
 957            from ..graphics.shape import Shape
 958
 959            closed = close_points2(poly1[0], poly1[-1])
 960            canvas.draw(Shape(poly1, closed=closed), fill=False)
 961            closed = close_points2(poly2[0], poly2[-1])
 962            canvas.draw(Shape(poly2, closed=closed), fill=False)
 963    return [poly1, poly2]
 964
 965
 966def offset_polygon_points(
 967    polygon: list[Point], offset: float = 1, dist_tol: float = None
 968) -> list[Point]:
 969    """
 970    Return a list of double offset lines from a list of lines.
 971
 972    Args:
 973        polygon (list[Point]): List of points representing the polygon.
 974        offset (float, optional): Offset distance. Defaults to 1.
 975        dist_tol (float, optional): Distance tolerance. Defaults to None.
 976
 977    Returns:
 978        list[Point]: List of double offset lines.
 979    """
 980    if dist_tol is None:
 981        dist_tol = defaults["dist_tol"]
 982    dist_tol2 = dist_tol * dist_tol
 983    polygon = list(polygon)
 984    if not close_points2(polygon[0], polygon[-1], dist2=dist_tol2):
 985        polygon.append(polygon[0])
 986    poly = []
 987    for i, point in enumerate(polygon[:-1]):
 988        line = [point, polygon[i + 1]]
 989        offset_edge = offset_line(line, offset)
 990        poly.append(offset_edge)
 991
 992    poly = stitch(poly)
 993    if not right_handed(poly):
 994        poly.reverse()
 995    return poly
 996
 997
 998def double_offset_lines(line: Line, offset: float = 1) -> tuple[Line, Line]:
 999    """
1000    Return two offset lines to a given line segment with the given offset amount.
1001
1002    Args:
1003        line (Line): Input line segment.
1004        offset (float, optional): Offset distance. Defaults to 1.
1005
1006    Returns:
1007        tuple[Line, Line]: Two offset lines.
1008    """
1009    line1 = offset_line(line, offset)
1010    line2 = offset_line(line, -offset)
1011
1012    return line1, line2
1013
1014
1015def equal_lines(line1: Line, line2: Line, dist_tol: float = None) -> bool:
1016    """
1017    Return True if two lines are close enough.
1018
1019    Args:
1020        line1 (Line): First line.
1021        line2 (Line): Second line.
1022        dist_tol (float, optional): Distance tolerance. Defaults to None.
1023
1024    Returns:
1025        bool: True if the lines are close enough, False otherwise.
1026    """
1027    if dist_tol is None:
1028        dist_tol = defaults["dist_tol"]
1029    dist_tol2 = dist_tol * dist_tol
1030    p1, p2 = line1
1031    p3, p4 = line2
1032    return (
1033        close_points2(p1, p3, dist2=dist_tol2)
1034        and close_points2(p2, p4, dist2=dist_tol2)
1035    ) or (
1036        close_points2(p1, p4, dist2=dist_tol2)
1037        and close_points2(p2, p3, dist2=dist_tol2)
1038    )
1039
1040
1041def equal_polygons(
1042    poly1: Sequence[Point], poly2: Sequence[Point], dist_tol: float = None
1043) -> bool:
1044    """
1045    Return True if two polygons are close enough.
1046
1047    Args:
1048        poly1 (Sequence[Point]): First polygon.
1049        poly2 (Sequence[Point]): Second polygon.
1050        dist_tol (float, optional): Distance tolerance. Defaults to None.
1051
1052    Returns:
1053        bool: True if the polygons are close enough, False otherwise.
1054    """
1055    if dist_tol is None:
1056        dist_tol = defaults["dist_tol"]
1057    if len(poly1) != len(poly2):
1058        return False
1059    dist_tol2 = dist_tol * dist_tol
1060    for i, pnt in enumerate(poly1):
1061        if not close_points2(pnt, poly2[i], dist2=dist_tol2):
1062            return False
1063    return True
1064
1065
1066def extended_line(dist: float, line: Line, extend_both=False) -> Line:
1067    """
1068    Given a line ((x1, y1), (x2, y2)) and a distance,
1069    the given line is extended by distance units.
1070    Return a new line ((x1, y1), (x2', y2')).
1071
1072    Args:
1073        dist (float): Distance to extend the line.
1074        line (Line): Input line.
1075        extend_both (bool, optional): Whether to extend both ends of the line. Defaults to False.
1076
1077    Returns:
1078        Line: Extended line.
1079    """
1080
1081    def extend(dist, line):
1082        # p = (1-t)*p1 + t*p2 : parametric equation of a line segment (p1, p2)
1083        line_length = length(line)
1084        t = (line_length + dist) / line_length
1085        p1, p2 = line
1086        x1, y1 = p1[:2]
1087        x2, y2 = p2[:2]
1088        c = 1 - t
1089
1090        return [(x1, y1), (c * x1 + t * x2, c * y1 + t * y2)]
1091
1092    if extend_both:
1093        p1, p2 = extend(dist, line)
1094        p1, p2 = extend(dist, [p2, p1])
1095        res = [p2, p1]
1096    else:
1097        res = extend(dist, line)
1098
1099    return res
1100
1101
1102def line_through_point_angle(
1103    point: Point, angle: float, length_: float, both_sides=False
1104) -> Line:
1105    """
1106    Return a line that passes through the given point
1107    with the given angle and length.
1108    If both_side is True, the line is extended on both sides by the given
1109    length.
1110
1111    Args:
1112        point (Point): Point through which the line passes.
1113        angle (float): Angle of the line in radians.
1114        length_ (float): Length of the line.
1115        both_sides (bool, optional): Whether to extend the line on both sides. Defaults to False.
1116
1117    Returns:
1118        Line: Line passing through the given point with the given angle and length.
1119    """
1120    x, y = point[:2]
1121    line = [(x, y), (x + length_ * cos(angle), y + length_ * sin(angle))]
1122    if both_sides:
1123        p1, p2 = line
1124        line = extended_line(length_, [p2, p1])
1125
1126    return line
1127
1128
1129def remove_duplicate_points(points: list[Point], dist_tol=None) -> list[Point]:
1130    """
1131    Return a list of points with duplicate points removed.
1132
1133    Args:
1134        points (list[Point]): List of points.
1135        dist_tol (float, optional): Distance tolerance. Defaults to None.
1136
1137    Returns:
1138        list[Point]: List of points with duplicate points removed.
1139    """
1140    if dist_tol is None:
1141        dist_tol = defaults["dist_tol"]
1142    new_points = []
1143    for i, point in enumerate(points):
1144        if i == 0:
1145            new_points.append(point)
1146        else:
1147            dist_tol2 = dist_tol * dist_tol
1148            if not close_points2(point, new_points[-1], dist2=dist_tol2):
1149                new_points.append(point)
1150    return new_points
1151
1152
1153def remove_collinear_points(
1154    points: list[Point], rtol: float = None, atol: float = None
1155) -> list[Point]:
1156    """
1157    Return a list of points with collinear points removed.
1158
1159    Args:
1160        points (list[Point]): List of points.
1161        rtol (float, optional): Relative tolerance. Defaults to None.
1162        atol (float, optional): Absolute tolerance. Defaults to None.
1163
1164    Returns:
1165        list[Point]: List of points with collinear points removed.
1166    """
1167    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
1168    new_points = []
1169    for i, point in enumerate(points):
1170        if i == 0:
1171            new_points.append(point)
1172        else:
1173            if not collinear(
1174                new_points[-1], point, points[(i + 1) % len(points)], rtol, atol
1175            ):
1176                new_points.append(point)
1177    return new_points
1178
1179
1180def fix_degen_points(
1181    points: list[Point],
1182    loop=False,
1183    closed=False,
1184    dist_tol: float = None,
1185    area_rtol: float = None,
1186    area_atol: float = None,
1187    check_collinear=True,
1188) -> list[Point]:
1189    """
1190    Return a list of points with duplicate points removed.
1191    Remove the middle point from the collinear points.
1192
1193    Args:
1194        points (list[Point]): List of points.
1195        loop (bool, optional): Whether to loop the points. Defaults to False.
1196        closed (bool, optional): Whether the points form a closed shape. Defaults to False.
1197        dist_tol (float, optional): Distance tolerance. Defaults to None.
1198        area_rtol (float, optional): Relative tolerance for area. Defaults to None.
1199        area_atol (float, optional): Absolute tolerance for area. Defaults to None.
1200        check_collinear (bool, optional): Whether to check for collinear points. Defaults to True.
1201
1202    Returns:
1203        list[Point]: List of points with duplicate and collinear points removed.
1204    """
1205    dist_tol, area_rtol, area_atol = get_defaults(
1206        ["dist_tol", "area_rtol", "area_atol"], [dist_tol, area_rtol, area_atol]
1207    )
1208    dist_tol2 = dist_tol * dist_tol
1209    new_points = []
1210    for i, point in enumerate(points):
1211        if i == 0:
1212            new_points.append(point)
1213        else:
1214            if not close_points2(point, new_points[-1], dist2=dist_tol2):
1215                new_points.append(point)
1216    if loop:
1217        if close_points2(new_points[0], new_points[-1], dist2=dist_tol2):
1218            new_points.pop(-1)
1219
1220    if check_collinear:
1221        # Check for collinear points and remove the middle one.
1222        new_points = merge_consecutive_collinear_edges(
1223            new_points, closed, area_rtol, area_atol
1224        )
1225
1226    return new_points
1227
1228
1229def clockwise(p: Point, q: Point, r: Point) -> bool:
1230    """Return 1 if the points p, q, and r are in clockwise order,
1231    return -1 if the points are in counter-clockwise order,
1232    return 0 if the points are collinear
1233
1234    Args:
1235        p (Point): First point.
1236        q (Point): Second point.
1237        r (Point): Third point.
1238
1239    Returns:
1240        int: 1 if the points are in clockwise order, -1 if counter-clockwise, 0 if collinear.
1241    """
1242    area_ = area(p, q, r)
1243    if area_ > 0:
1244        res = 1
1245    elif area_ < 0:
1246        res = -1
1247    else:
1248        res = 0
1249
1250    return res
1251
1252
1253def intersects(seg1, seg2):
1254    """Checks if the line segments intersect.
1255    If they are chained together, they are considered as intersecting.
1256    Returns True if the segments intersect, False otherwise.
1257
1258    Args:
1259        seg1 (Line): First line segment.
1260        seg2 (Line): Second line segment.
1261
1262    Returns:
1263        bool: True if the segments intersect, False otherwise.
1264    """
1265    p1, q1 = seg1
1266    p2, q2 = seg2
1267    o1 = clockwise(p1, q1, p2)
1268    o2 = clockwise(p1, q1, q2)
1269    o3 = clockwise(p2, q2, p1)
1270    o4 = clockwise(p2, q2, q1)
1271
1272    if o1 != o2 and o3 != o4:
1273        return True
1274
1275    if o1 == 0 and between(p1, p2, q1):
1276        return True
1277    if o2 == 0 and between(p1, q2, q1):
1278        return True
1279    if o3 == 0 and between(p2, p1, q2):
1280        return True
1281    if o4 == 0 and between(p2, q1, q2):
1282        return True
1283
1284    return False
1285
1286
1287def is_chained(seg1, seg2):
1288    """Checks if the line segments are chained together.
1289
1290    Args:
1291        seg1 (Line): First line segment.
1292        seg2 (Line): Second line segment.
1293
1294    Returns:
1295        bool: True if the segments are chained together, False otherwise.
1296    """
1297    p1, q1 = seg1
1298    p2, q2 = seg2
1299    if (
1300        close_points2(p1, p2)
1301        or close_points2(p1, q2)
1302        or close_points2(q1, p2)
1303        or close_points2(q1, q2)
1304    ):
1305        return True
1306
1307    return False
1308
1309
1310def direction(p, q, r):
1311    """
1312    Checks the orientation of three points (p, q, r).
1313
1314    Args:
1315        p (Point): First point.
1316        q (Point): Second point.
1317        r (Point): Third point.
1318
1319    Returns:
1320        int: 0 if collinear, >0 if counter-clockwise, <0 if clockwise.
1321    """
1322    return (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1])
1323
1324
1325def collinear_segments(segment1, segment2, tol=None, atol=None):
1326    """
1327    Checks if two line segments (a1, b1) and (a2, b2) are collinear.
1328
1329    Args:
1330        segment1 (Line): First line segment.
1331        segment2 (Line): Second line segment.
1332        tol (float, optional): Relative tolerance. Defaults to None.
1333        atol (float, optional): Absolute tolerance. Defaults to None.
1334
1335    Returns:
1336        bool: True if the segments are collinear, False otherwise.
1337    """
1338    tol, atol = get_defaults(["tol", "atol"], [tol, atol])
1339    a1, b1 = segment1
1340    a2, b2 = segment2
1341
1342    return isclose(direction(a1, b1, a2), 0, tol, atol) and isclose(
1343        direction(a1, b1, b2), 0, tol, atol
1344    )
1345
1346
1347def global_to_local(
1348    x: float, y: float, xi: float, yi: float, theta: float = 0
1349) -> Point:
1350    """Given a point(x, y) in global coordinates
1351    and local CS position and orientation,
1352    return a point(ksi, eta) in local coordinates
1353
1354    Args:
1355        x (float): Global x-coordinate.
1356        y (float): Global y-coordinate.
1357        xi (float): Local x-coordinate.
1358        yi (float): Local y-coordinate.
1359        theta (float, optional): Angle in radians. Defaults to 0.
1360
1361    Returns:
1362        Point: Local coordinates (ksi, eta).
1363    """
1364    sin_theta = sin(theta)
1365    cos_theta = cos(theta)
1366    ksi = (x - xi) * cos_theta + (y - yi) * sin_theta
1367    eta = (y - yi) * cos_theta - (x - xi) * sin_theta
1368    return (ksi, eta)
1369
1370
1371def stitch_lines(line1: Line, line2: Line) -> Sequence[Line]:
1372    """if the lines intersect, trim the lines
1373    if the lines don't intersect, extend the lines
1374
1375    Args:
1376        line1 (Line): First line.
1377        line2 (Line): Second line.
1378
1379    Returns:
1380        Sequence[Line]: Trimmed or extended lines.
1381    """
1382    intersection_ = intersect(line1, line2)
1383    res = None
1384    if intersection_:
1385        p1, _ = line1
1386        _, p2 = line2
1387        line1 = [p1, intersection_]
1388        line2 = [intersection_, p2]
1389
1390        res = (line1, line2)
1391
1392    return res
1393
1394
1395def get_quadrant(x: float, y: float) -> int:
1396    """quadrants:
1397    +x, +y = 1st
1398    +x, -y = 2nd
1399    -x, -y = 3rd
1400    +x, -y = 4th
1401
1402    Args:
1403        x (float): x-coordinate.
1404        y (float): y-coordinate.
1405
1406    Returns:
1407        int: Quadrant number.
1408    """
1409    return int(floor((atan2(y, x) % (TWO_PI)) / (pi / 2)) + 1)
1410
1411
1412def get_quadrant_from_deg_angle(deg_angle: float) -> int:
1413    """quadrants:
1414    (0, 90) = 1st
1415    (90, 180) = 2nd
1416    (180, 270) = 3rd
1417    (270, 360) = 4th
1418
1419    Args:
1420        deg_angle (float): Angle in degrees.
1421
1422    Returns:
1423        int: Quadrant number.
1424    """
1425    return int(floor(deg_angle / 90.0) % 4 + 1)
1426
1427
1428def homogenize(points: Sequence[Point]) -> 'ndarray':
1429    """
1430    Convert a list of points to homogeneous coordinates.
1431
1432    Args:
1433        points (Sequence[Point]): List of points.
1434
1435    Returns:
1436        np.ndarray: Homogeneous coordinates.
1437    """
1438    try:
1439        xy_array = np.array(points, dtype=float)
1440    except ValueError:
1441        xy_array = np.array([p[:2] for p in points], dtype=float)
1442    n_rows, n_cols = xy_array.shape
1443    if n_cols > 2:
1444        xy_array = xy_array[:, :2]
1445    ones = np.ones((n_rows, 1), dtype=float)
1446    homogeneous_array = np.append(xy_array, ones, axis=1)
1447
1448    return homogeneous_array
1449
1450
1451
1452def _homogenize(coordinates: Sequence[float]) -> 'ndarray':
1453    """Internal use only. API provides a homogenize function.
1454    Given a sequence of coordinates(x1, y1, x2, y2, ... xn, yn),
1455    return a numpy array of points array(((x1, y1, 1.),
1456    (x2, y2, 1.), ... (xn, yn, 1.))).
1457
1458    Args:
1459        coordinates (Sequence[float]): Sequence of coordinates.
1460
1461    Returns:
1462        np.ndarray: Homogeneous coordinates.
1463    """
1464    xy_array = np.array(list(zip(coordinates[0::2], coordinates[1::2])), dtype=float)
1465    n_rows = xy_array.shape[0]
1466    ones = np.ones((n_rows, 1), dtype=float)
1467    homogeneous_array = np.append(xy_array, ones, axis=1)
1468
1469    return homogeneous_array
1470
1471
1472def intersect2(
1473    x1: float,
1474    y1: float,
1475    x2: float,
1476    y2: float,
1477    x3: float,
1478    y3: float,
1479    x4: float,
1480    y4: float,
1481    rtol: float = None,
1482    atol: float = None,
1483) -> Point:
1484    """Return the intersection point of two lines.
1485    line1: (x1, y1), (x2, y2)
1486    line2: (x3, y3), (x4, y4)
1487    To find the intersection point of two line segments use the
1488    "intersection" function
1489
1490    Args:
1491        x1 (float): x-coordinate of the first point of the first line.
1492        y1 (float): y-coordinate of the first point of the first line.
1493        x2 (float): x-coordinate of the second point of the first line.
1494        y2 (float): y-coordinate of the second point of the first line.
1495        x3 (float): x-coordinate of the first point of the second line.
1496        y3 (float): y-coordinate of the first point of the second line.
1497        x4 (float): x-coordinate of the second point of the second line.
1498        y4 (float): y-coordinate of the second point of the second line.
1499        rtol (float, optional): Relative tolerance. Defaults to None.
1500        atol (float, optional): Absolute tolerance. Defaults to None.
1501
1502    Returns:
1503        Point: Intersection point of the two lines.
1504    """
1505    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
1506    x1_x2 = x1 - x2
1507    y1_y2 = y1 - y2
1508    x3_x4 = x3 - x4
1509    y3_y4 = y3 - y4
1510
1511    denom = (x1_x2) * (y3_y4) - (y1_y2) * (x3_x4)
1512    if isclose(denom, 0, rtol=rtol, atol=atol):
1513        res = None  # parallel lines
1514    else:
1515        x = ((x1 * y2 - y1 * x2) * (x3_x4) - (x1_x2) * (x3 * y4 - y3 * x4)) / denom
1516        y = ((x1 * y2 - y1 * x2) * (y3_y4) - (y1_y2) * (x3 * y4 - y3 * x4)) / denom
1517        res = (x, y)
1518
1519    return res
1520
1521
1522def intersect(line1: Line, line2: Line) -> Point:
1523    """Return the intersection point of two lines.
1524    line1: [(x1, y1), (x2, y2)]
1525    line2: [(x3, y3), (x4, y4)]
1526    To find the intersection point of two line segments use the
1527    "intersection" function
1528
1529    Args:
1530        line1 (Line): First line.
1531        line2 (Line): Second line.
1532
1533    Returns:
1534        Point: Intersection point of the two lines.
1535    """
1536    x1, y1 = line1[0][:2]
1537    x2, y2 = line1[1][:2]
1538    x3, y3 = line2[0][:2]
1539    x4, y4 = line2[1][:2]
1540    return intersect2(x1, y1, x2, y2, x3, y3, x4, y4)
1541
1542
1543def intersection2(x1, y1, x2, y2, x3, y3, x4, y4, rtol=None, atol=None):
1544    """Check the intersection of two line segments. See the documentation
1545
1546    Args:
1547        x1 (float): x-coordinate of the first point of the first line segment.
1548        y1 (float): y-coordinate of the first point of the first line segment.
1549        x2 (float): x-coordinate of the second point of the first line segment.
1550        y2 (float): y-coordinate of the second point of the first line segment.
1551        x3 (float): x-coordinate of the first point of the second line segment.
1552        y3 (float): y-coordinate of the first point of the second line segment.
1553        x4 (float): x-coordinate of the second point of the second line segment.
1554        y4 (float): y-coordinate of the second point of the second line segment.
1555        rtol (float, optional): Relative tolerance. Defaults to None.
1556        atol (float, optional): Absolute tolerance. Defaults to None.
1557
1558    Returns:
1559        tuple: Connection type and intersection point.
1560    """
1561    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
1562    x2_x1 = x2 - x1
1563    y2_y1 = y2 - y1
1564    x4_x3 = x4 - x3
1565    y4_y3 = y4 - y3
1566    denom = (y4_y3) * (x2_x1) - (x4_x3) * (y2_y1)
1567    if isclose(denom, 0, rtol=rtol, atol=atol):  # parallel
1568        return Connection.PARALLEL, None
1569    x1_x3 = x1 - x3
1570    y1_y3 = y1 - y3
1571    ua = ((x4_x3) * (y1_y3) - (y4_y3) * (x1_x3)) / denom
1572    if ua < 0 or ua > 1:
1573        return Connection.DISJOINT, None
1574    ub = ((x2_x1) * (y1_y3) - (y2_y1) * (x1_x3)) / denom
1575    if ub < 0 or ub > 1:
1576        return Connection.DISJOINT, None
1577    x = x1 + ua * (x2_x1)
1578    y = y1 + ua * (y2_y1)
1579    return Connection.INTERSECT, (x, y)
1580
1581
1582def intersection3(
1583    x1: float,
1584    y1: float,
1585    x2: float,
1586    y2: float,
1587    x3: float,
1588    y3: float,
1589    x4: float,
1590    y4: float,
1591    rtol: float = None,
1592    atol: float = None,
1593    dist_tol: float = None,
1594    area_atol: float = None,
1595) -> tuple[Connection, list]:
1596    """Check the intersection of two line segments. See the documentation
1597    for more details.
1598
1599    Args:
1600        x1 (float): x-coordinate of the first point of the first line segment.
1601        y1 (float): y-coordinate of the first point of the first line segment.
1602        x2 (float): x-coordinate of the second point of the first line segment.
1603        y2 (float): y-coordinate of the second point of the first line segment.
1604        x3 (float): x-coordinate of the first point of the second line segment.
1605        y3 (float): y-coordinate of the first point of the second line segment.
1606        x4 (float): x-coordinate of the second point of the second line segment.
1607        y4 (float): y-coordinate of the second point of the second line segment.
1608        rtol (float, optional): Relative tolerance. Defaults to None.
1609        atol (float, optional): Absolute tolerance. Defaults to None.
1610        dist_tol (float, optional): Distance tolerance. Defaults to None.
1611        area_atol (float, optional): Absolute tolerance for area. Defaults to None.
1612
1613    Returns:
1614        tuple: Connection type and intersection result.
1615    """
1616    # collinear check uses area_atol
1617
1618    # s1: start1 = (x1, y1)
1619    # e1: end1 = (x2, y2)
1620    # s2: start2 = (x3, y3)
1621    # e2: end2 = (x4, y4)
1622    # s1s2: start1 and start2 is connected
1623    # s1e2: start1 and end2 is connected
1624    # e1s2: end1 and start2 is connected
1625    # e1e2: end1 and end2 is connected
1626    rtol, atol, dist_tol, area_atol = get_defaults(
1627        ["rtol", "atol", "dist_tol", "area_atol"], [rtol, atol, dist_tol, area_atol]
1628    )
1629
1630    s1 = (x1, y1)
1631    e1 = (x2, y2)
1632    s2 = (x3, y3)
1633    e2 = (x4, y4)
1634    segment1 = [(x1, y1), (x2, y2)]
1635    segment2 = [(x3, y3), (x4, y4)]
1636
1637    # check if the segments' bounding boxes overlap
1638    if not line_segment_bbox_check(segment1, segment2):
1639        return (Connection.DISJOINT, None)
1640
1641    # Check if the segments are parallel
1642    x2_x1 = x2 - x1
1643    y2_y1 = y2 - y1
1644    x4_x3 = x4 - x3
1645    y4_y3 = y4 - y3
1646    denom = (y4_y3) * (x2_x1) - (x4_x3) * (y2_y1)
1647    parallel = isclose(denom, 0, rtol=rtol, atol=atol)
1648    # angle1 = atan2(y2 - y1, x2 - x1) % pi
1649    # angle2 = atan2(y4 - y3, x4 - x3) % pi
1650    # parallel = close_angles(angle1, angle2, angtol=defaults['angtol'])
1651
1652    # Coincident end points
1653    dist_tol2 = dist_tol * dist_tol
1654    s1s2 = close_points2(s1, s2, dist2=dist_tol2)
1655    s1e2 = close_points2(s1, e2, dist2=dist_tol2)
1656    e1s2 = close_points2(e1, s2, dist2=dist_tol2)
1657    e1e2 = close_points2(e1, e2, dist2=dist_tol2)
1658    connected = s1s2 or s1e2 or e1s2 or e1e2
1659    if parallel:
1660        length1 = distance((x1, y1), (x2, y2))
1661        length2 = distance((x3, y3), (x4, y4))
1662        min_x = min(x1, x2, x3, x4)
1663        max_x = max(x1, x2, x3, x4)
1664        min_y = min(y1, y2, y3, y4)
1665        max_y = max(y1, y2, y3, y4)
1666        total_length = distance((min_x, min_y), (max_x, max_y))
1667        l1_eq_l2 = isclose(length1, length2, rtol=rtol, atol=atol)
1668        l1_eq_total = isclose(length1, total_length, rtol=rtol, atol=atol)
1669        l2_eq_total = isclose(length2, total_length, rtol=rtol, atol=atol)
1670        if connected:
1671            if l1_eq_l2 and l1_eq_total:
1672                return Connection.CONGRUENT, segment1
1673
1674            if l1_eq_total:
1675                return Connection.CONTAINS, segment1
1676            if l2_eq_total:
1677                return Connection.WITHIN, segment2
1678            if isclose(length1 + length2, total_length, rtol, atol):
1679                # chained and collienar
1680                if s1s2:
1681                    return Connection.COLL_CHAIN, (e1, s1, e2)
1682                if s1e2:
1683                    return Connection.COLL_CHAIN, (e1, s1, s2)
1684                if e1s2:
1685                    return Connection.COLL_CHAIN, (s1, s2, e2)
1686                if e1e2:
1687                    return Connection.COLL_CHAIN, (s1, e1, s2)
1688        else:
1689            if total_length < length1 + length2 and collinear_segments(
1690                segment1, segment2, atol
1691            ):
1692                p1 = (min_x, min_y)
1693                p2 = (max_x, max_y)
1694                seg = [p1, p2]
1695                return Connection.OVERLAPS, seg
1696
1697            return intersection2(x1, y1, x2, y2, x3, y3, x4, y4, rtol, atol)
1698    else:
1699        if connected:
1700            if s1s2:
1701                return Connection.CHAIN, (e1, s1, e2)
1702            if s1e2:
1703                return Connection.CHAIN, (e1, s1, s2)
1704            if e1s2:
1705                return Connection.CHAIN, (s1, s2, e2)
1706            if e1e2:
1707                return Connection.CHAIN, (s1, e1, s2)
1708        else:
1709            if between(s1, e1, e2):
1710                return Connection.YJOINT, e1
1711            if between(s1, e1, s2):
1712                return Connection.YJOINT, s1
1713            if between(s2, e2, e1):
1714                return Connection.YJOINT, e2
1715            if between(s2, e2, s1):
1716                return Connection.YJOINT, s2
1717
1718            return intersection2(x1, y1, x2, y2, x3, y3, x4, y4, rtol, atol)
1719    return (Connection.DISJOINT, None)
1720
1721
1722def merge_consecutive_collinear_edges(
1723    points, closed=False, area_rtol=None, area_atol=None
1724):
1725    """Remove the middle points from collinear edges.
1726
1727    Args:
1728        points (list[Point]): List of points.
1729        closed (bool, optional): Whether the points form a closed shape. Defaults to False.
1730        area_rtol (float, optional): Relative tolerance for area. Defaults to None.
1731        area_atol (float, optional): Absolute tolerance for area. Defaults to None.
1732
1733    Returns:
1734        list[Point]: List of points with collinear points removed.
1735    """
1736    area_rtol, area_atol = get_defaults(
1737        ["area_rtol", "area_atol"], [area_rtol, area_atol]
1738    )
1739    points = points[:]
1740
1741    while True:
1742        cyc = cycle(points)
1743        a = next(cyc)
1744        b = next(cyc)
1745        c = next(cyc)
1746        looping = False
1747        n = len(points) - 1
1748        if closed:
1749            n += 1
1750        discarded = []
1751        for _ in range(n - 1):
1752            if collinear(a, b, c, area_rtol=area_rtol, area_atol=area_atol):
1753                discarded.append(b)
1754                looping = True
1755                break
1756            a = b
1757            b = c
1758            c = next(cyc)
1759        for point in discarded:
1760            points.remove(point)
1761        if not looping or len(points) < 3:
1762            break
1763
1764    return points
1765
1766
1767def intersection(line1: Line, line2: Line, rtol: float = None) -> int:
1768    """return the intersection point of two line segments.
1769    segment1: ((x1, y1), (x2, y2))
1770    segment2: ((x3, y3), (x4, y4))
1771    if line segments do not intersect return -1
1772    if line segments are parallel return 0
1773    if line segments are connected (share a point) return 1
1774    To find the intersection point of two lines use the "intersect" function
1775
1776    Args:
1777        line1 (Line): First line segment.
1778        line2 (Line): Second line segment.
1779        rtol (float, optional): Relative tolerance. Defaults to None.
1780
1781    Returns:
1782        int: Intersection type.
1783    """
1784    if rtol is None:
1785        rtol = defaults["rtol"]
1786    x1, y1 = line1[0]
1787    x2, y2 = line1[1]
1788    x3, y3 = line2[0]
1789    x4, y4 = line2[1]
1790    return intersection2(x1, y1, x2, y2, x3, y3, x4, y4)
1791
1792
1793def merge_segments(seg1: Sequence[Point], seg2: Sequence[Point]) -> Sequence[Point]:
1794    """Merge two segments into one segment if they are connected.
1795    They need to be overlapping or simply connected to each other,
1796    otherwise they will not be merged. Order doesn't matter.
1797
1798    Args:
1799        seg1 (Sequence[Point]): First segment.
1800        seg2 (Sequence[Point]): Second segment.
1801
1802    Returns:
1803        Sequence[Point]: Merged segment.
1804    """
1805    Conn = Connection
1806    p1, p2 = seg1
1807    p3, p4 = seg2
1808
1809    res = all_intersections([(p1, p2), (p3, p4)], use_intersection3=True)
1810    if res:
1811        conn_type = list(res.values())[0][0][0]
1812        verts = list(res.values())[0][0][1]
1813        if conn_type in [Conn.OVERLAPS, Conn.CONGRUENT, Conn.CHAIN]:
1814            res = verts
1815        elif conn_type == Conn.COLL_CHAIN:
1816            res = (verts[0], verts[1])
1817        else:
1818            res = None
1819    else:
1820        res = None  # need this to avoid returning an empty dict
1821
1822    return res
1823
1824
1825def invert(p, center, radius):
1826    """Inverts p about a circle at the given center and radius
1827
1828    Args:
1829        p (Point): Point to invert.
1830        center (Point): Center of the circle.
1831        radius (float): Radius of the circle.
1832
1833    Returns:
1834        Point: Inverted point.
1835    """
1836    dist = distance(p, center)
1837    if dist == 0:
1838        return p
1839    p = np.array(p)
1840    center = np.array(center)
1841    return center + (radius**2 / dist**2) * (p - center)
1842    # return radius**2 * (p - center) / dist
1843
1844
1845def is_horizontal(line: Line, eps: float = 0.0001) -> bool:
1846    """Return True if the line is horizontal.
1847
1848    Args:
1849        line (Line): Input line.
1850        eps (float, optional): Tolerance. Defaults to 0.0001.
1851
1852    Returns:
1853        bool: True if the line is horizontal, False otherwise.
1854    """
1855    return abs(j_vec.dot(line_vector(line))) <= eps
1856
1857
1858def is_line(line_: Any) -> bool:
1859    """Return True if the input is a line.
1860
1861    Args:
1862        line_ (Any): Input value.
1863
1864    Returns:
1865        bool: True if the input is a line, False otherwise.
1866    """
1867    try:
1868        p1, p2 = line_
1869        return is_point(p1) and is_point(p2)
1870    except:
1871        return False
1872
1873
1874def is_point(pnt: Any) -> bool:
1875    """Return True if the input is a point.
1876
1877    Args:
1878        pnt (Any): Input value.
1879
1880    Returns:
1881        bool: True if the input is a point, False otherwise.
1882    """
1883    try:
1884        x, y = pnt[:2]
1885        return is_number(x) and is_number(y)
1886    except:
1887        return False
1888
1889
1890def is_vertical(line: Line, eps: float = 0.0001) -> bool:
1891    """Return True if the line is vertical.
1892
1893    Args:
1894        line (Line): Input line.
1895        eps (float, optional): Tolerance. Defaults to 0.0001.
1896
1897    Returns:
1898        bool: True if the line is vertical, False otherwise.
1899    """
1900    return abs(i_vec.dot(line_vector(line))) <= eps
1901
1902
1903def length(line: Line) -> float:
1904    """Return the length of a line.
1905
1906    Args:
1907        line (Line): Input line.
1908
1909    Returns:
1910        float: Length of the line.
1911    """
1912    p1, p2 = line
1913    return distance(p1, p2)
1914
1915
1916def lerp_point(p1: Point, p2: Point, t: float) -> Point:
1917    """Linear interpolation of two points.
1918
1919    Args:
1920        p1 (Point): First point.
1921        p2 (Point): Second point.
1922        t (float): Interpolation parameter. t = 0 => p1, t = 1 => p2.
1923
1924    Returns:
1925        Point: Interpolated point.
1926    """
1927    x1, y1 = p1
1928    x2, y2 = p2
1929    return (lerp(x1, x2, t), lerp(y1, y2, t))
1930
1931
1932def slope(start_point: Point, end_point: Point, rtol=None, atol=None) -> float:
1933    """Return the slope of a line given by two points.
1934    Order makes a difference.
1935
1936    Args:
1937        start_point (Point): Start point of the line.
1938        end_point (Point): End point of the line.
1939        rtol (float, optional): Relative tolerance. Defaults to None.
1940        atol (float, optional): Absolute tolerance. Defaults to None.
1941
1942    Returns:
1943        float: Slope of the line.
1944    """
1945    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
1946    x1, y1 = start_point[:2]
1947    x2, y2 = end_point[:2]
1948    if isclose(x1, x2, rtol=rtol, atol=atol):
1949        res = defaults["INF"]
1950    else:
1951        res = (y2 - y1) / (x2 - x1)
1952
1953    return res
1954
1955
1956def segmentize_line(line: Line, segment_length: float) -> list[Line]:
1957    """Return a list of points that would form segments with the given length.
1958
1959    Args:
1960        line (Line): Input line.
1961        segment_length (float): Length of each segment.
1962
1963    Returns:
1964        list[Line]: List of segments.
1965    """
1966    length_ = distance(line[0], line[1])
1967    x1, y1 = line[0]
1968    x2, y2 = line[1]
1969    increments = int(length_ / segment_length)
1970    x_segments = np.linspace(x1, x2, increments)
1971    y_segments = np.linspace(y1, y2, increments)
1972
1973    return list(zip(x_segments, y_segments))
1974
1975
1976def line_angle(start_point: Point, end_point: Point) -> float:
1977    """Return the orientation angle (in radians) of a line given by start and end points.
1978    Order makes a difference.
1979
1980    Args:
1981        start_point (Point): Start point of the line.
1982        end_point (Point): End point of the line.
1983
1984    Returns:
1985        float: Orientation angle of the line in radians.
1986    """
1987    return atan2(end_point[1] - start_point[1], end_point[0] - start_point[0])
1988
1989
1990def inclination_angle(start_point: Point, end_point: Point) -> float:
1991    """Return the inclination angle (in radians) of a line given by start and end points.
1992    Inclination angle is always between zero and pi.
1993    Order makes no difference.
1994
1995    Args:
1996        start_point (Point): Start point of the line.
1997        end_point (Point): End point of the line.
1998
1999    Returns:
2000        float: Inclination angle of the line in radians.
2001    """
2002    return line_angle(start_point, end_point) % pi
2003
2004
2005def line2vector(line: Line) -> VecType:
2006    """Return the vector representation of a line
2007
2008    Args:
2009        line (Line): Input line.
2010
2011    Returns:
2012        VecType: Vector representation of the line.
2013    """
2014    x1, y1 = line[0]
2015    x2, y2 = line[1]
2016    dx = x2 - x1
2017    dy = y2 - y1
2018    return [dx, dy]
2019
2020
2021def line_through_point_and_angle(
2022    point: Point, angle: float, length_: float = 100
2023) -> Line:
2024    """Return a line through the given point with the given angle and length
2025
2026    Args:
2027        point (Point): Point through which the line passes.
2028        angle (float): Angle of the line in radians.
2029        length_ (float, optional): Length of the line. Defaults to 100.
2030
2031    Returns:
2032        Line: Line passing through the given point with the given angle and length.
2033    """
2034    x, y = point[:2]
2035    dx = length_ * cos(angle)
2036    dy = length_ * sin(angle)
2037    return [[x, y], [x + dx, y + dy]]
2038
2039
2040def line_vector(line: Line) -> VecType:
2041    """Return the vector representation of a line.
2042
2043    Args:
2044        line (Line): Input line.
2045
2046    Returns:
2047        VecType: Vector representation of the line.
2048    """
2049    x1, y1 = line[0]
2050    x2, y2 = line[1]
2051    return Vector2D(x2 - x1, y2 - y1)
2052
2053
2054def mid_point(p1: Point, p2: Point) -> Point:
2055    """Return the mid point of two points.
2056
2057    Args:
2058        p1 (Point): First point.
2059        p2 (Point): Second point.
2060
2061    Returns:
2062        Point: Mid point of the two points.
2063    """
2064    x = (p2[0] + p1[0]) / 2
2065    y = (p2[1] + p1[1]) / 2
2066    return (x, y)
2067
2068
2069def norm(vec: VecType) -> float:
2070    """Return the norm (vector length) of a vector.
2071
2072    Args:
2073        vec (VecType): Input vector.
2074
2075    Returns:
2076        float: Norm of the vector.
2077    """
2078    return hypot(vec[0], vec[1])
2079
2080
2081def ndarray_to_xy_list(arr: 'ndarray') -> Sequence[Point]:
2082    """Convert a numpy array to a list of points.
2083
2084    Args:
2085        arr (np.ndarray): Input numpy array.
2086
2087    Returns:
2088        Sequence[Point]: List of points.
2089    """
2090    return arr[:, :2].tolist()
2091
2092
2093def offset_line(line: Sequence[Point], offset: float) -> Sequence[Point]:
2094    """Return an offset line from a given line.
2095
2096    Args:
2097        line (Sequence[Point]): Input line.
2098        offset (float): Offset distance.
2099
2100    Returns:
2101        Sequence[Point]: Offset line.
2102    """
2103    unit_vec = perp_unit_vector(line)
2104    dx = unit_vec[0] * offset
2105    dy = unit_vec[1] * offset
2106    x1, y1 = line[0]
2107    x2, y2 = line[1]
2108    return [[x1 + dx, y1 + dy], [x2 + dx, y2 + dy]]
2109
2110
2111def offset_lines(polylines: Sequence[Line], offset: float = 1) -> list[Line]:
2112    """Return a list of offset lines from a list of lines.
2113
2114    Args:
2115        polylines (Sequence[Line]): List of input lines.
2116        offset (float, optional): Offset distance. Defaults to 1.
2117
2118    Returns:
2119        list[Line]: List of offset lines.
2120    """
2121
2122    def stitch_(polyline):
2123        res = []
2124        line1 = polyline[0]
2125        for i, _ in enumerate(polyline):
2126            if i == len(polyline) - 1:
2127                break
2128            line2 = polyline[i + 1]
2129            line1, line2 = stitch_lines(line1, line2)
2130            res.extend(line1)
2131            line1 = line2
2132        res.append(line2[-1])
2133        return res
2134
2135    poly = []
2136    for line in polylines:
2137        poly.append(offset_line(line, offset))
2138    poly = stitch_(poly)
2139    return poly
2140
2141
2142def normalize(vec: VecType) -> VecType:
2143    """Return the normalized vector.
2144
2145    Args:
2146        vec (VecType): Input vector.
2147
2148    Returns:
2149        VecType: Normalized vector.
2150    """
2151    norm_ = norm(vec)
2152    return [vec[0] / norm_, vec[1] / norm_]
2153
2154
2155def offset_point_on_line(point: Point, line: Line, offset: float) -> Point:
2156    """Return a point on a line that is offset from the given point.
2157
2158    Args:
2159        point (Point): Input point.
2160        line (Line): Input line.
2161        offset (float): Offset distance.
2162
2163    Returns:
2164        Point: Offset point on the line.
2165    """
2166    x, y = point[:2]
2167    x1, y1 = line[0]
2168    x2, y2 = line[1]
2169    dx = x2 - x1
2170    dy = y2 - y1
2171    # normalize the vector
2172    mag = (dx * dx + dy * dy) ** 0.5
2173    dx = dx / mag
2174    dy = dy / mag
2175    return x + dx * offset, y + dy * offset
2176
2177
2178def offset_point(point: Point, dx: float = 0, dy: float = 0) -> Point:
2179    """Return an offset point from a given point.
2180
2181    Args:
2182        point (Point): Input point.
2183        dx (float, optional): Offset distance in x-direction. Defaults to 0.
2184        dy (float, optional): Offset distance in y-direction. Defaults to 0.
2185
2186    Returns:
2187        Point: Offset point.
2188    """
2189    x, y = point[:2]
2190    return x + dx, y + dy
2191
2192
2193def parallel_line(line: Line, point: Point) -> Line:
2194    """Return a parallel line to the given line that goes through the given point
2195
2196    Args:
2197        line (Line): Input line.
2198        point (Point): Point through which the parallel line passes.
2199
2200    Returns:
2201        Line: Parallel line.
2202    """
2203    x1, y1 = line[0]
2204    x2, y2 = line[1]
2205    x3, y3 = point
2206    dx = x2 - x1
2207    dy = y2 - y1
2208    return [[x3, y3], [x3 + dx, y3 + dy]]
2209
2210
2211def perp_offset_point(point: Point, line: Line, offset: float) -> Point:
2212    """Return a point that is offset from the given point in the perpendicular direction to the given line.
2213
2214    Args:
2215        point (Point): Input point.
2216        line (Line): Input line.
2217        offset (float): Offset distance.
2218
2219    Returns:
2220        Point: Perpendicular offset point.
2221    """
2222    unit_vec = perp_unit_vector(line)
2223    dx = unit_vec[0] * offset
2224    dy = unit_vec[1] * offset
2225    x, y = point[:2]
2226    return [x + dx, y + dy]
2227
2228
2229def perp_unit_vector(line: Line) -> VecType:
2230    """Return the perpendicular unit vector to a line
2231
2232    Args:
2233        line (Line): Input line.
2234
2235    Returns:
2236        VecType: Perpendicular unit vector.
2237    """
2238    x1, y1 = line[0]
2239    x2, y2 = line[1]
2240    dx = x2 - x1
2241    dy = y2 - y1
2242    norm_ = sqrt(dx**2 + dy**2)
2243    return [-dy / norm_, dx / norm_]
2244
2245
2246def point_on_line(
2247    point: Point, line: Line, rtol: float = None, atol: float = None
2248) -> bool:
2249    """Return True if the given point is on the given line
2250
2251    Args:
2252        point (Point): Input point.
2253        line (Line): Input line.
2254        rtol (float, optional): Relative tolerance. Defaults to None.
2255        atol (float, optional): Absolute tolerance. Defaults to None.
2256
2257    Returns:
2258        bool: True if the point is on the line, False otherwise.
2259    """
2260    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
2261    p1, p2 = line
2262    return isclose(slope(p1, point), slope(point, p2), rtol=rtol, atol=atol)
2263
2264
2265def point_on_line_segment(
2266    point: Point, line: Line, rtol: float = None, atol: float = None
2267) -> bool:
2268    """Return True if the given point is on the given line segment
2269
2270    Args:
2271        point (Point): Input point.
2272        line (Line): Input line segment.
2273        rtol (float, optional): Relative tolerance. Defaults to None.
2274        atol (float, optional): Absolute tolerance. Defaults to None.
2275
2276    Returns:
2277        bool: True if the point is on the line segment, False otherwise.
2278    """
2279    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
2280    p1, p2 = line
2281    return isclose(
2282        (distance(p1, point) + distance(p2, point)),
2283        distance(p1, p2),
2284        rtol=rtol,
2285        atol=atol,
2286    )
2287
2288
2289def point_to_line_distance(point: Point, line: Line) -> float:
2290    """Return the vector from a point to a line
2291
2292    Args:
2293        point (Point): Input point.
2294        line (Line): Input line.
2295
2296    Returns:
2297        float: Distance from the point to the line.
2298    """
2299    x0, y0 = point
2300    x1, y1 = line[0]
2301    x2, y2 = line[1]
2302    dx = x2 - x1
2303    dy = y2 - y1
2304    return abs((dx * (y1 - y0) - (x1 - x0) * dy)) / sqrt(dx**2 + dy**2)
2305
2306
2307def point_to_line_seg_distance(p, lp1, lp2):
2308    """Given a point p and a line segment defined by boundary points
2309    lp1 and lp2, returns the distance between the line segment and the point.
2310    If the point is not located in the perpendicular area between the
2311    boundary points, returns False.
2312
2313    Args:
2314        p (Point): Input point.
2315        lp1 (Point): First boundary point of the line segment.
2316        lp2 (Point): Second boundary point of the line segment.
2317
2318    Returns:
2319        float: Distance between the point and the line segment, or False if the point is not in the perpendicular area.
2320    """
2321    if lp1[:2] == lp2[:2]:
2322        msg = "Error! Line is ill defined. Start and end points are coincident."
2323        raise ValueError(msg)
2324    x3, y3 = p[:2]
2325    x1, y1 = lp1[:2]
2326    x2, y2 = lp2[:2]
2327
2328    u = ((x3 - x1) * (x2 - x1) + (y3 - y1) * (y2 - y1)) / distance(lp1, lp2) ** 2
2329    if 0 <= u <= 1:
2330        x = x1 + u * (x2 - x1)
2331        y = y1 + u * (y2 - y1)
2332        res = distance((x, y), p)
2333    else:
2334        res = False  # p is not between lp1 and lp2
2335
2336    return res
2337
2338def rotate_point(point: Point, angle: float, center: Point = (0, 0)) -> Point:
2339    x, y = point[:2]
2340    cx, cy = center[:2]
2341    x -= cx
2342    y -= cy
2343    cos_angle = cos(angle)
2344    sin_angle = sin(angle)
2345    x, y = x*cos_angle - y*sin_angle, x*sin_angle + y*cos_angle
2346    x += cx
2347    y += cy
2348
2349    return (x, y)
2350
2351
2352def point_to_line_vec(point: Point, line: Line, unit: bool = False) -> VecType:
2353    """Return the perpendicular vector from a point to a line
2354
2355    Args:
2356        point (Point): Input point.
2357        line (Line): Input line.
2358        unit (bool, optional): Whether to return a unit vector. Defaults to False.
2359
2360    Returns:
2361        VecType: Perpendicular vector from the point to the line.
2362    """
2363    x0, y0 = point
2364    x1, y1 = line[0]
2365    x2, y2 = line[1]
2366    dx = x2 - x1
2367    dy = y2 - y1
2368    norm_ = sqrt(dx**2 + dy**2)
2369    unit_vec = [-dy / norm_, dx / norm_]
2370    dist = (dx * (y1 - y0) - (x1 - x0) * dy) / sqrt(dx**2 + dy**2)
2371    if unit:
2372        if dist > 0:
2373            res = [unit_vec[0], unit_vec[1]]
2374        else:
2375            res = [-unit_vec[0], -unit_vec[1]]
2376    else:
2377        res = [unit_vec[0] * dist, unit_vec[1] * dist]
2378
2379    return res
2380
2381
2382def polygon_area(polygon: Sequence[Point], dist_tol=None) -> float:
2383    """Calculate the area of a polygon.
2384
2385    Args:
2386        polygon (Sequence[Point]): List of points representing the polygon.
2387        dist_tol (float, optional): Distance tolerance. Defaults to None.
2388
2389    Returns:
2390        float: Area of the polygon.
2391    """
2392    if dist_tol is None:
2393        dist_tol = defaults["dist_tol"]
2394    dist_tol2 = dist_tol * dist_tol
2395    if not close_points2(polygon[0], polygon[-1], dist2=dist_tol2):
2396        polygon = list(polygon[:])
2397        polygon.append(polygon[0])
2398    area_ = 0
2399    for i, point in enumerate(polygon[:-1]):
2400        x1, y1 = point
2401        x2, y2 = polygon[i + 1]
2402        area_ += x1 * y2 - x2 * y1
2403    return area_ / 2
2404
2405
2406def polyline_length(polygon: Sequence[Point], closed=False, dist_tol=None) -> float:
2407    """Calculate the perimeter of a polygon.
2408
2409    Args:
2410        polygon (Sequence[Point]): List of points representing the polygon.
2411        closed (bool, optional): Whether the polygon is closed. Defaults to False.
2412        dist_tol (float, optional): Distance tolerance. Defaults to None.
2413
2414    Returns:
2415        float: Perimeter of the polygon.
2416    """
2417    if dist_tol is None:
2418        dist_tol = defaults["dist_tol"]
2419    dist_tol2 = dist_tol * dist_tol
2420    if closed:
2421        if not close_points2(polygon[0], polygon[-1], dist2=dist_tol2):
2422            polygon = polygon[:]
2423            polygon.append(polygon[0])
2424    perimeter = 0
2425    for i, point in enumerate(polygon[:-1]):
2426        perimeter += distance(point, polygon[i + 1])
2427    return perimeter
2428
2429
2430def right_handed(polygon: Sequence[Point], dist_tol=None) -> float:
2431    """If polygon is counter-clockwise, return True
2432
2433    Args:
2434        polygon (Sequence[Point]): List of points representing the polygon.
2435        dist_tol (float, optional): Distance tolerance. Defaults to None.
2436
2437    Returns:
2438        bool: True if the polygon is counter-clockwise, False otherwise.
2439    """
2440    if dist_tol is None:
2441        dist_tol = defaults["dist_tol"]
2442    dist_tol2 = dist_tol * dist_tol
2443    added_point = False
2444    if not close_points2(polygon[0], polygon[-1], dist2=dist_tol2):
2445        polygon.append(polygon[0])
2446        added_point = True
2447    area_ = 0
2448    for i, point in enumerate(polygon[:-1]):
2449        x1, y1 = point
2450        x2, y2 = polygon[i + 1]
2451        area_ += x1 * y2 - x2 * y1
2452    if added_point:
2453        polygon.pop()
2454    return area_ > 0
2455
2456
2457def radius2side_len(n: int, radius: float) -> float:
2458    """Given a radius and the number of sides, return the side length
2459    of an n-sided regular polygon with the given radius
2460
2461    Args:
2462        n (int): Number of sides.
2463        radius (float): Radius of the polygon.
2464
2465    Returns:
2466        float: Side length of the polygon.
2467    """
2468    return 2 * radius * sin(pi / n)
2469
2470
2471def tokenize_svg_path(path: str) -> list[str]:
2472    """Tokenize an SVG path string.
2473
2474    Args:
2475        path (str): SVG path string.
2476
2477    Returns:
2478        list[str]: List of tokens.
2479    """
2480    return re.findall(r"[a-zA-Z]|[-+]?\d*\.\d+|\d+", path)
2481
2482
2483def law_of_cosines(a: float, b: float, c: float) -> float:
2484    """Return the angle of a triangle given the three sides.
2485    Returns the angle of A in radians. A is the angle between
2486    sides b and c.
2487    cos(A) = (b^2 + c^2 - a^2) / (2 * b * c)
2488
2489    Args:
2490        a (float): Length of side a.
2491        b (float): Length of side b.
2492        c (float): Length of side c.
2493
2494    Returns:
2495        float: Angle of A in radians.
2496    """
2497    return acos((b**2 + c**2 - a**2) / (2 * b * c))
2498
2499
2500def segmentize_catmull_rom(
2501    a: float, b: float, c: float, d: float, n: int = 100
2502) -> Sequence[float]:
2503    """a and b are the control points and c and d are
2504    start and end points respectively,
2505    n is the number of segments to generate.
2506
2507    Args:
2508        a (float): First control point.
2509        b (float): Second control point.
2510        c (float): Start point.
2511        d (float): End point.
2512        n (int, optional): Number of segments to generate. Defaults to 100.
2513
2514    Returns:
2515        Sequence[float]: List of points representing the segments.
2516    """
2517    a = array(a[:2], dtype=float)
2518    b = array(b[:2], dtype=float)
2519    c = array(c[:2], dtype=float)
2520    d = array(d[:2], dtype=float)
2521
2522    t = 0
2523    dt = 1.0 / n
2524    points = []
2525    term1 = 2 * b
2526    term2 = -a + c
2527    term3 = 2 * a - 5 * b + 4 * c - d
2528    term4 = -a + 3 * b - 3 * c + d
2529
2530    for _ in range(n + 1):
2531        q = 0.5 * (term1 + term2 * t + term3 * t**2 + term4 * t**3)
2532        points.append([q[0], q[1]])
2533        t += dt
2534    return points
2535
2536
2537def side_len_to_radius(n: int, side_len: float) -> float:
2538    """Given a side length and the number of sides, return the radius
2539    of an n-sided regular polygon with the given side_len length
2540
2541    Args:
2542        n (int): Number of sides.
2543        side_len (float): Side length of the polygon.
2544
2545    Returns:
2546        float: Radius of the polygon.
2547    """
2548    return side_len / (2 * sin(pi / n))
2549
2550
2551def translate_line(dx: float, dy: float, line: Line) -> Line:
2552    """Return a translated line by dx and dy
2553
2554    Args:
2555        dx (float): Translation distance in x-direction.
2556        dy (float): Translation distance in y-direction.
2557        line (Line): Input line.
2558
2559    Returns:
2560        Line: Translated line.
2561    """
2562    x1, y1 = line[0]
2563    x2, y2 = line[1]
2564    return [[x1 + dx, y1 + dy], [x2 + dx, y2 + dy]]
2565
2566
2567def trim_line(line1: Line, line2: Line) -> Line:
2568    """Trim line1 to the intersection of line1 and line2.
2569    Extend it if necessary.
2570
2571    Args:
2572        line1 (Line): First line.
2573        line2 (Line): Second line.
2574
2575    Returns:
2576        Line: Trimmed line.
2577    """
2578    intersection_ = intersection(line1, line2)
2579    return [line1[0], intersection_]
2580
2581
2582def unit_vector(line: Line) -> VecType:
2583    """Return the unit vector of a line
2584
2585    Args:
2586        line (Line): Input line.
2587
2588    Returns:
2589        VecType: Unit vector of the line.
2590    """
2591    norm_ = length(line)
2592    p1, p2 = line
2593    x1, y1 = p1
2594    x2, y2 = p2
2595    return [(x2 - x1) / norm_, (y2 - y1) / norm_]
2596
2597
2598def unit_vector_(line: Line) -> Sequence[VecType]:
2599    """Return the cartesian unit vector of a line
2600    with the given line's start and end points
2601
2602    Args:
2603        line (Line): Input line.
2604
2605    Returns:
2606        Sequence[VecType]: Cartesian unit vector of the line.
2607    """
2608    x1, y1 = line[0]
2609    x2, y2 = line[1]
2610    dx = x2 - x1
2611    dy = y2 - y1
2612    norm_ = sqrt(dx**2 + dy**2)
2613    return [dx / norm_, dy / norm_]
2614
2615
2616def vec_along_line(line: Line, magnitude: float) -> VecType:
2617    """Return a vector along a line with the given magnitude.
2618
2619    Args:
2620        line (Line): Input line.
2621        magnitude (float): Magnitude of the vector.
2622
2623    Returns:
2624        VecType: Vector along the line with the given magnitude.
2625    """
2626    if line == axis_x:
2627        dx, dy = magnitude, 0
2628    elif line == axis_y:
2629        dx, dy = 0, magnitude
2630    else:
2631        # line is (p1, p2)
2632        theta = line_angle(*line)
2633        dx = magnitude * cos(theta)
2634        dy = magnitude * sin(theta)
2635    return dx, dy
2636
2637
2638def vec_dir_angle(vec: Sequence[float]) -> float:
2639    """Return the direction angle of a vector
2640
2641    Args:
2642        vec (Sequence[float]): Input vector.
2643
2644    Returns:
2645        float: Direction angle of the vector.
2646    """
2647    return atan2(vec[1], vec[0])
2648
2649
2650def cross_product_sense(a: Point, b: Point, c: Point) -> int:
2651    """Return the cross product sense of vectors a and b.
2652
2653    Args:
2654        a (Point): First point.
2655        b (Point): Second point.
2656        c (Point): Third point.
2657
2658    Returns:
2659        int: Cross product sense.
2660    """
2661    length_ = cross_product2(a, b, c)
2662    if length_ == 0:
2663        res = 1
2664    else:
2665        res = length_ / abs(length)
2666
2667    return res
2668
2669
2670#      A
2671#      /
2672#     /
2673#   B/
2674#    \
2675#     \
2676#      \
2677#       C
2678
2679
2680def right_turn(p1, p2, p3):
2681    """Return True if p1, p2, p3 make a right turn.
2682
2683    Args:
2684        p1 (Point): First point.
2685        p2 (Point): Second point.
2686        p3 (Point): Third point.
2687
2688    Returns:
2689        bool: True if the points make a right turn, False otherwise.
2690    """
2691    return cross(p1, p2, p3) < 0
2692
2693
2694def left_turn(p1, p2, p3):
2695    """Return True if p1, p2, p3 make a left turn.
2696
2697    Args:
2698        p1 (Point): First point.
2699        p2 (Point): Second point.
2700        p3 (Point): Third point.
2701
2702    Returns:
2703        bool: True if the points make a left turn, False otherwise.
2704    """
2705    return cross(p1, p2, p3) > 0
2706
2707
2708def cross(p1, p2, p3):
2709    """Return the cross product of vectors p1p2 and p1p3.
2710
2711    Args:
2712        p1 (Point): First point.
2713        p2 (Point): Second point.
2714        p3 (Point): Third point.
2715
2716    Returns:
2717        float: Cross product of the vectors.
2718    """
2719    x1, y1 = p2[0] - p1[0], p2[1] - p1[1]
2720    x2, y2 = p3[0] - p1[0], p3[1] - p1[1]
2721    return x1 * y2 - x2 * y1
2722
2723
2724def tri_to_cart(points):
2725    """
2726    Convert a list of points from triangular to cartesian coordinates.
2727
2728    Args:
2729        points (list[Point]): List of points in triangular coordinates.
2730
2731    Returns:
2732        np.ndarray: List of points in cartesian coordinates.
2733    """
2734    u = [1, 0]
2735    v = cos(pi / 3), sin(pi / 3)
2736    convert = array([u, v])
2737
2738    return array(points) @ convert
2739
2740
2741def cart_to_tri(points):
2742    """
2743    Convert a list of points from cartesian to triangular coordinates.
2744
2745    Args:
2746        points (list[Point]): List of points in cartesian coordinates.
2747
2748    Returns:
2749        np.ndarray: List of points in triangular coordinates.
2750    """
2751    u = [1, 0]
2752    v = cos(pi / 3), sin(pi / 3)
2753    convert = np.linalg.inv(array([u, v]))
2754
2755    return array(points) @ convert
2756
2757
2758def convex_hull(points):
2759    """Return the convex hull of a set of 2D points.
2760
2761    Args:
2762        points (list[Point]): List of 2D points.
2763
2764    Returns:
2765        list[Point]: Convex hull of the points.
2766    """
2767    # From http://en.wikibooks.org/wiki/Algorithm__implementation/Geometry/
2768    # Convex_hull/Monotone_chain
2769    # Sort points lexicographically (tuples are compared lexicographically).
2770    # Remove duplicates to detect the case we have just one unique point.
2771    points = sorted(set(points))
2772    # Boring case: no points or a single point, possibly repeated multiple times.
2773    if len(points) <= 1:
2774        return points
2775
2776    # 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross
2777    # product.
2778    # Return a positive value, if OAB makes a counter-clockwise turn,
2779    # negative for clockwise turn, and zero if the points are collinear.
2780    def cross_(o, a, b):
2781        return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
2782
2783    # Build lower hull
2784    lower = []
2785    for p in points:
2786        while len(lower) >= 2 and cross_(lower[-2], lower[-1], p) <= 0:
2787            lower.pop()
2788        lower.append(p)
2789    # Build upper hull
2790    upper = []
2791    for p in reversed(points):
2792        while len(upper) >= 2 and cross_(upper[-2], upper[-1], p) <= 0:
2793            upper.pop()
2794        upper.append(p)
2795    # Concatenation of the lower and upper hulls gives the convex hull.
2796    # Last point of each list is omitted because it is repeated at the beginning
2797    # of the other list.
2798    return lower[:-1] + upper[:-1]
2799
2800
2801def connected_pairs(items):
2802    """Return a list of connected pair tuples corresponding to the items.
2803    [a, b, c] -> [(a, b), (b, c)]
2804
2805    Args:
2806        items (list): List of items.
2807
2808    Returns:
2809        list[tuple]: List of connected pair tuples.
2810    """
2811    return list(zip(items, items[1:]))
2812
2813
2814def flat_points(connected_segments):
2815    """Return a list of points from a list of connected pairs of points.
2816
2817    Args:
2818        connected_segments (list[tuple]): List of connected pairs of points.
2819
2820    Returns:
2821        list[Point]: List of points.
2822    """
2823    points = [line[0] for line in connected_segments]
2824    points.append(connected_segments[-1][1])
2825    return points
2826
2827
2828def point_in_quad(point: Point, quad: list[Point]) -> bool:
2829    """Return True if the point is inside the quad.
2830
2831    Args:
2832        point (Point): Input point.
2833        quad (list[Point]): List of points representing the quad.
2834
2835    Returns:
2836        bool: True if the point is inside the quad, False otherwise.
2837    """
2838    x, y = point[:2]
2839    x1, y1 = quad[0]
2840    x2, y2 = quad[1]
2841    x3, y3 = quad[2]
2842    x4, y4 = quad[3]
2843    xs = [x1, x2, x3, x4]
2844    ys = [y1, y2, y3, y4]
2845    min_x = min(xs)
2846    max_x = max(xs)
2847    min_y = min(ys)
2848    max_y = max(ys)
2849    return min_x <= x <= max_x and min_y <= y <= max_y
2850
2851
2852def get_polygons(
2853    nested_points: Sequence[Point], n_round_digits: int = 2, dist_tol: float = None
2854) -> list:
2855    """Convert points to clean polygons. Points are vertices of polygons.
2856
2857    Args:
2858        nested_points (Sequence[Point]): List of nested points.
2859        n_round_digits (int, optional): Number of decimal places to round to. Defaults to 2.
2860        dist_tol (float, optional): Distance tolerance. Defaults to None.
2861
2862    Returns:
2863        list: List of clean polygons.
2864    """
2865    if dist_tol is None:
2866        dist_tol = defaults["dist_tol"]
2867    from ..graph import get_cycles
2868
2869    nested_rounded_points = []
2870    for points in nested_points:
2871        rounded_points = []
2872        for point in points:
2873            rounded_point = (around(point, n_round_digits)).tolist()
2874            rounded_points.append(tuple(rounded_point))
2875        nested_rounded_points.append(rounded_points)
2876
2877    s_points = set()
2878    d_id__point = {}
2879    d_point__id = {}
2880    for points in nested_rounded_points:
2881        for point in points:
2882            s_points.add(point)
2883
2884    for i, fs_point in enumerate(s_points):
2885        d_id__point[i] = fs_point  # we need a bidirectional dictionary
2886        d_point__id[fs_point] = i
2887
2888    nested_point_ids = []
2889    for points in nested_rounded_points:
2890        point_ids = []
2891        for point in points:
2892            point_ids.append(d_point__id[point])
2893        nested_point_ids.append(point_ids)
2894
2895    graph_edges = []
2896    for point_ids in nested_point_ids:
2897        graph_edges.extend(connected_pairs(point_ids))
2898    polygons = []
2899    graph_edges = sanitize_graph_edges(graph_edges)
2900    cycles = get_cycles(graph_edges)
2901    if cycles is None:
2902        return []
2903    for cycle_ in cycles:
2904        nodes = cycle_
2905        points = [d_id__point[i] for i in nodes]
2906        points = fix_degen_points(points, closed=True, dist_tol=dist_tol)
2907        polygons.append(points)
2908
2909    return polygons
2910
2911
2912def offset_point_from_start(p1, p2, offset):
2913    """p1, p2: points on a line
2914    offset: distance from p1
2915    return the point on the line at the given offset
2916
2917    Args:
2918        p1 (Point): First point on the line.
2919        p2 (Point): Second point on the line.
2920        offset (float): Distance from p1.
2921
2922    Returns:
2923        Point: Point on the line at the given offset.
2924    """
2925    x1, y1 = p1
2926    x2, y2 = p2
2927    dx, dy = x2 - x1, y2 - y1
2928    d = (dx**2 + dy**2) ** 0.5
2929    if d == 0:
2930        res = p1
2931    else:
2932        res = (x1 + offset * dx / d, y1 + offset * dy / d)
2933
2934    return res
2935
2936
2937def angle_between_two_lines(line1, line2):
2938    """Return the angle between two lines in radians.
2939
2940    Args:
2941        line1 (Line): First line.
2942        line2 (Line): Second line.
2943
2944    Returns:
2945        float: Angle between the two lines in radians.
2946    """
2947    alpha1 = line_angle(*line1)
2948    alpha2 = line_angle(*line2)
2949    return abs(alpha1 - alpha2)
2950
2951
2952def rotate_point(point:Point, angle:float, center:Point=(0, 0)):
2953    """Rotate a point around a center by an angle in radians.
2954
2955    Args:
2956        point (Point): Point to rotate.
2957        angle (float): Angle of rotation in radians.
2958        center (Point): Center of rotation.
2959
2960    Returns:
2961        Point: Rotated point.
2962    """
2963    x, y = point[:2]
2964    cx, cy = center[:2]
2965    dx = x - cx
2966    dy = y - cy
2967    x = cx + dx * cos(angle) - dy * sin(angle)
2968    y = cy + dx * sin(angle) + dy * cos(angle)
2969    return (x, y)
2970
2971
2972def circle_tangent_to2lines(line1, line2, intersection_, radius):
2973    """Given two lines, their intersection point and a radius,
2974    return the center of the circle tangent to both lines and
2975    with the given radius.
2976
2977    Args:
2978        line1 (Line): First line.
2979        line2 (Line): Second line.
2980        intersection_ (Point): Intersection point of the lines.
2981        radius (float): Radius of the circle.
2982
2983    Returns:
2984        tuple: Center of the circle, start and end points of the tangent lines.
2985    """
2986    alpha = angle_between_two_lines(line1, line2)
2987    dist = radius / sin(alpha / 2)
2988    start = offset_point_from_start(intersection_, line1.p1, dist)
2989    center = rotate_point(start, intersection_, alpha / 2)
2990    end = offset_point_from_start(intersection_, line2.p1, dist)
2991
2992    return center, start, end
2993
2994
2995def triangle_area(a: float, b: float, c: float) -> float:
2996    """
2997    Given side lengths a, b and c, return the area of the triangle.
2998
2999    Args:
3000        a (float): Length of side a.
3001        b (float): Length of side b.
3002        c (float): Length of side c.
3003
3004    Returns:
3005        float: Area of the triangle.
3006    """
3007    a_b = a - b
3008    return sqrt((a + (b + c)) * (c - (a_b)) * (c + (a_b)) * (a + (b - c))) / 4
3009
3010
3011def round_point(point: list[float], n_digits: int = 2) -> list[float]:
3012    """
3013    Round a point (x, y) to a given precision.
3014
3015    Args:
3016        point (list[float]): Input point.
3017        n_digits (int, optional): Number of decimal places to round to. Defaults to 2.
3018
3019    Returns:
3020        list[float]: Rounded point.
3021    """
3022    x, y = point[:2]
3023    x = round(x, n_digits)
3024    y = round(y, n_digits)
3025    return (x, y)
3026
3027
3028def round_segment(segment: Sequence[Point], n_digits: int = 2):
3029    """Round a segment to a given precision.
3030
3031    Args:
3032        segment (Sequence[Point]): Input segment.
3033        n_digits (int, optional): Number of decimal places to round to. Defaults to 2.
3034
3035    Returns:
3036        Sequence[Point]: Rounded segment.
3037    """
3038    p1 = round_point(segment[0], n_digits)
3039    p2 = round_point(segment[1], n_digits)
3040
3041    return [p1, p2]
3042
3043
3044def get_polygon_grid_point(n, line1, line2, circumradius=100):
3045    """See chapter ??? for explanation of this function.
3046
3047    Args:
3048        n (int): Number of sides.
3049        line1 (Line): First line.
3050        line2 (Line): Second line.
3051        circumradius (float, optional): Circumradius of the polygon. Defaults to 100.
3052
3053    Returns:
3054        Point: Grid point of the polygon.
3055    """
3056    s = circumradius * 2 * sin(pi / n)  # side length
3057    points = reg_poly_points(0, 0, n, s)[:-1]
3058    p1 = points[line1[0]]
3059    p2 = points[line1[1]]
3060    p3 = points[line2[0]]
3061    p4 = points[line2[1]]
3062
3063    return intersection((p1, p2), (p3, p4))[1]
3064
3065
3066def congruent_polygons(
3067    polygon1: list[Point],
3068    polygon2: list[Point],
3069    dist_tol: float = None,
3070    area_tol: float = None,
3071    side_length_tol: float = None,
3072    angle_tol: float = None,
3073) -> bool:
3074    """
3075    Return True if two polygons are congruent.
3076    They can be translated, rotated and/or reflected.
3077
3078    Args:
3079        polygon1 (list[Point]): First polygon.
3080        polygon2 (list[Point]): Second polygon.
3081        dist_tol (float, optional): Distance tolerance. Defaults to None.
3082        area_tol (float, optional): Area tolerance. Defaults to None.
3083        side_length_tol (float, optional): Side length tolerance. Defaults to None.
3084        angle_tol (float, optional): Angle tolerance. Defaults to None.
3085
3086    Returns:
3087        bool: True if the polygons are congruent, False otherwise.
3088    """
3089    dist_tol, area_tol, angle_tol = get_defaults(
3090        ["dist_tol", "area_rtol", "angle_rtol"], [dist_tol, area_tol, angle_tol]
3091    )
3092    if side_length_tol is None:
3093        side_length_tol = defaults["rtol"]
3094    dist_tol2 = dist_tol * dist_tol
3095    poly1 = polygon1
3096    poly2 = polygon2
3097    if close_points2(poly1[0], poly1[-1], dist2=dist_tol2):
3098        poly1 = poly1[:-1]
3099    if close_points2(poly2[0], poly2[-1], dist2=dist_tol2):
3100        poly2 = poly2[:-1]
3101    len_poly1 = len(poly1)
3102    len_poly2 = len(poly2)
3103    if len_poly1 != len_poly2:
3104        return False
3105    if not isclose(
3106        abs(polygon_area(poly1)), abs(polygon_area(poly2)), rtol=area_tol, atol=area_tol
3107    ):
3108        return False
3109
3110    side_lengths1 = [distance(poly1[i], poly1[i - 1]) for i in range(len_poly1)]
3111    side_lengths2 = [distance(poly2[i], poly2[i - 1]) for i in range(len_poly2)]
3112    check1 = equal_cycles(side_lengths1, side_lengths2, rtol=side_length_tol)
3113    if not check1:
3114        check_reverse = equal_cycles(
3115            side_lengths1, side_lengths2[::-1], rtol=side_length_tol
3116        )
3117        if not (check1 or check_reverse):
3118            return False
3119
3120    angles1 = polygon_internal_angles(poly1)
3121    angles2 = polygon_internal_angles(poly2)
3122    check1 = equal_cycles(angles1, angles2, angle_tol)
3123    if not check1:
3124        poly2 = poly2[::-1]
3125        angles2 = polygon_internal_angles(poly2)
3126        check_reverse = equal_cycles(angles1, angles2, angle_tol)
3127        if not (check1 or check_reverse):
3128            return False
3129
3130    return True
3131
3132
3133def positive_angle(angle, radians=True, tol=None, atol=None):
3134    """Return the positive angle in radians or degrees.
3135
3136    Args:
3137        angle (float): Input angle.
3138        radians (bool, optional): Whether the angle is in radians. Defaults to True.
3139        tol (float, optional): Relative tolerance. Defaults to None.
3140        atol (float, optional): Absolute tolerance. Defaults to None.
3141
3142    Returns:
3143        float: Positive angle.
3144    """
3145    tol, atol = get_defaults(["tol", "rtol"], [tol, atol])
3146    if radians:
3147        if angle < 0:
3148            angle += 2 * pi
3149        # if isclose(angle, TWO_PI, rtol=tol, atol=atol):
3150        #     angle = 0
3151    else:
3152        if angle < 0:
3153            angle += 360
3154        # if isclose(angle, 360, rtol=tol, atol=atol):
3155        #     angle = 0
3156    return angle
3157
3158
3159def polygon_internal_angles(polygon):
3160    """Return the internal angles of a polygon.
3161
3162    Args:
3163        polygon (list[Point]): List of points representing the polygon.
3164
3165    Returns:
3166        list[float]: List of internal angles of the polygon.
3167    """
3168    angles = []
3169    len_polygon = len(polygon)
3170    for i, pnt in enumerate(polygon):
3171        p1 = polygon[i - 1]
3172        p2 = pnt
3173        p3 = polygon[(i + 1) % len_polygon]
3174        angles.append(angle_between_lines2(p1, p2, p3))
3175
3176    return angles
3177
3178
3179def bisector_line(a: Point, b: Point, c: Point) -> Line:
3180    """
3181    Given three points that form two lines [a, b] and [b, c]
3182    return the bisector line between them.
3183
3184    Args:
3185        a (Point): First point.
3186        b (Point): Second point.
3187        c (Point): Third point.
3188
3189    Returns:
3190        Line: Bisector line.
3191    """
3192    d = mid_point(a, c)
3193
3194    return [d, b]
3195
3196
3197def between(a, b, c):
3198    """Return True if c is between a and b.
3199
3200    Args:
3201        a (Point): First point.
3202        b (Point): Second point.
3203        c (Point): Third point.
3204
3205    Returns:
3206        bool: True if c is between a and b, False otherwise.
3207    """
3208    if not collinear(a, b, c):
3209        res = False
3210    elif a[0] != b[0]:
3211        res = ((a[0] <= c[0]) and (c[0] <= b[0])) or ((a[0] >= c[0]) and (c[0] >= b[0]))
3212    else:
3213        res = ((a[1] <= c[1]) and (c[1] <= b[1])) or ((a[1] >= c[1]) and (c[1] >= b[1]))
3214    return res
3215
3216
3217def collinear(a, b, c, area_rtol=None, area_atol=None):
3218    """Return True if a, b, and c are collinear.
3219
3220    Args:
3221        a (Point): First point.
3222        b (Point): Second point.
3223        c (Point): Third point.
3224        area_rtol (float, optional): Relative tolerance for area. Defaults to None.
3225        area_atol (float, optional): Absolute tolerance for area. Defaults to None.
3226
3227    Returns:
3228        bool: True if the points are collinear, False otherwise.
3229    """
3230    area_rtol, area_atol = get_defaults(
3231        ["area_rtol", "area_atol"], [area_rtol, area_atol]
3232    )
3233    return isclose(area(a, b, c), 0, rtol=area_rtol, atol=area_atol)
3234
3235
3236def polar_to_cartesian(r, theta):
3237    """Convert polar coordinates to cartesian coordinates.
3238
3239    Args:
3240        r (float): Radius.
3241        theta (float): Angle in radians.
3242
3243    Returns:
3244        Point: Cartesian coordinates.
3245    """
3246    return (r * cos(theta), r * sin(theta))
3247
3248
3249def cartesian_to_polar(x, y):
3250    """Convert cartesian coordinates to polar coordinates.
3251
3252    Args:
3253        x (float): x-coordinate.
3254        y (float): y-coordinate.
3255
3256    Returns:
3257        tuple: Polar coordinates (r, theta).
3258    """
3259    r = hypot(x, y)
3260    theta = positive_angle(atan2(y, x))
3261    return r, theta
3262
3263
3264def fillet(a: Point, b: Point, c: Point, radius: float) -> tuple[Line, Line, Point]:
3265    """
3266    Given three points that form two lines [a, b] and [b, c]
3267    return the clipped lines [a, d], [e, c], center point
3268    of the radius circle (tangent to both lines), and the arc
3269    angle of the formed fillet.
3270
3271    Args:
3272        a (Point): First point.
3273        b (Point): Second point.
3274        c (Point): Third point.
3275        radius (float): Radius of the fillet.
3276
3277    Returns:
3278        tuple: Clipped lines [a, d], [e, c], center point of the radius circle, and the arc angle.
3279    """
3280    alpha2 = angle_between_lines2(a, b, c) / 2
3281    sin_alpha2 = sin(alpha2)
3282    cos_alpha2 = cos(alpha2)
3283    clip_length = radius * cos_alpha2 / sin_alpha2
3284    d = offset_point_from_start(b, a, clip_length)
3285    e = offset_point_from_start(b, c, clip_length)
3286    mp = mid_point(a, c)  # [b, mp] is the bisector line
3287    center = offset_point_from_start(b, mp, radius / sin_alpha2)
3288    arc_angle = angle_between_lines2(e, center, d)
3289
3290    return [a, d], [e, c], center, arc_angle
3291
3292
3293def line_by_point_angle_length(point, angle, length_):
3294    """
3295    Given a point, an angle, and a length, return the line
3296    that starts at the point and has the given angle and length.
3297
3298    Args:
3299        point (Point): Start point of the line.
3300        angle (float): Angle of the line in radians.
3301        length_ (float): Length of the line.
3302
3303    Returns:
3304        Line: Line with the given angle and length.
3305    """
3306    x, y = point[:2]
3307    dx = length_ * cos(angle)
3308    dy = length_ * sin(angle)
3309
3310    return [(x, y), (x + dx, y + dy)]
3311
3312
3313def surface_normal(p1: Point, p2: Point, p3: Point) -> VecType:
3314    """
3315    Calculates the surface normal of a triangle given its vertices.
3316
3317    Args:
3318        p1 (Point): First vertex.
3319        p2 (Point): Second vertex.
3320        p3 (Point): Third vertex.
3321
3322    Returns:
3323        VecType: Surface normal vector.
3324    """
3325    v1 = np.array(p1)
3326    v2 = np.array(p2)
3327    v3 = np.array(p3)
3328    # Create two vectors from the vertices
3329    u = v2 - v1
3330    v = v3 - v1
3331
3332    # Calculate the cross product of the two vectors
3333    normal = np.cross(u, v)
3334
3335    # Normalize the vector to get a unit normal vector
3336    normal = normal / np.linalg.norm(normal)
3337
3338    return normal
3339
3340
3341def normal(point1, point2):
3342    """Return the normal vector of a line.
3343
3344    Args:
3345        point1 (Point): First point of the line.
3346        point2 (Point): Second point of the line.
3347
3348    Returns:
3349        VecType: Normal vector of the line.
3350    """
3351    x1, y1 = point1
3352    x2, y2 = point2
3353    dx = x2 - x1
3354    dy = y2 - y1
3355    norm = sqrt(dx**2 + dy**2)
3356    return [-dy / norm, dx / norm]
3357
3358
3359def area(a, b, c):
3360    """Return the area of a triangle given its vertices.
3361
3362    Args:
3363        a (Point): First vertex.
3364        b (Point): Second vertex.
3365        c (Point): Third vertex.
3366
3367    Returns:
3368        float: Area of the triangle.
3369    """
3370    return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])
3371
3372
3373def calc_area(points):
3374    """Calculate the area of a simple polygon (given by a list of its vertices).
3375
3376    Args:
3377        points (list[Point]): List of points representing the polygon.
3378
3379    Returns:
3380        tuple: Area of the polygon and whether it is clockwise.
3381    """
3382    area_ = 0
3383    n_points = len(points)
3384    for i in range(n_points):
3385        v = points[i]
3386        vnext = points[(i + 1) % n_points]
3387        area_ += v[0] * vnext[1] - vnext[0] * v[1]
3388    clockwise = area_ > 0
3389
3390    return (abs(area_ / 2.0), clockwise)
3391
3392
3393def remove_bad_points(points):
3394    """Remove redundant and collinear points from a list of points.
3395
3396    Args:
3397        points (list[Point]): List of points.
3398
3399    Returns:
3400        list[Point]: List of points with redundant and collinear points removed.
3401    """
3402    EPSILON = 1e-16
3403    n_points = len(points)
3404    # check for redundant points
3405    for i, p in enumerate(points[:]):
3406        for j in range(i + 1, n_points - 1):
3407            if p == points[j]:  # then remove the redundant point
3408                # maybe we should display a warning message here indicating
3409                # that redundant point is removed!!!
3410                points.remove(p)
3411
3412    n_points = len(points)
3413    # check for three consecutive points on a line
3414    lin_points = []
3415    for i in range(2, n_points - 1):
3416        if EPSILON > calc_area([points[i - 2], points[i - 1], points[i]])[0] > -EPSILON:
3417            lin_points.append(points[i - 1])
3418
3419    if EPSILON > calc_area([points[-2], points[-1], points[0]])[0] > -EPSILON:
3420        lin_points.append(points[-1])
3421
3422    for p in lin_points:
3423        # maybe we should display a warning message here indicating that linear
3424        # point is removed!!!
3425        points.remove(p)
3426
3427    return points
3428
3429
3430def is_convex(points):
3431    """Return True if the polygon is convex.
3432
3433    Args:
3434        points (list[Point]): List of points representing the polygon.
3435
3436    Returns:
3437        bool: True if the polygon is convex, False otherwise.
3438    """
3439    points = remove_bad_points(points)
3440    n_checks = len(points)
3441    points = points + [points[0]]
3442    senses = []
3443    for i in range(n_checks):
3444        if i == (n_checks - 1):
3445            senses.append(cross_product_sense(points[i], points[0], points[1]))
3446        else:
3447            senses.append(cross_product_sense(points[i], points[i + 1], points[i + 2]))
3448    s = set(senses)
3449    return len(s) == 1
3450
3451
3452def set_vertices(points):
3453    """Set the next and previous vertices of a list of vertices.
3454
3455    Args:
3456        points (list[Vertex]): List of vertices.
3457    """
3458    if not isinstance(points[0], Vertex):
3459        points = [Vertex(*p[:]) for p in points]
3460    n_points = len(points)
3461    for i, p in enumerate(points):
3462        if i == 0:
3463            p.prev = points[-1]
3464            p.next = points[i + 1]
3465        elif i == (n_points - 1):
3466            p.prev = points[i - 1]
3467            p.next = points[0]
3468        else:
3469            p.prev = points[i - 1]
3470            p.next = points[i + 1]
3471        p.angle = cross_product_sense(p.prev, p, p.next)
3472
3473
3474def circle_circle_intersections(x0, y0, r0, x1, y1, r1):
3475    """Return the intersection points of two circles.
3476
3477    Args:
3478        x0 (float): x-coordinate of the center of the first circle.
3479        y0 (float): y-coordinate of the center of the first circle.
3480        r0 (float): Radius of the first circle.
3481        x1 (float): x-coordinate of the center of the second circle.
3482        y1 (float): y-coordinate of the center of the second circle.
3483        r1 (float): Radius of the second circle.
3484
3485    Returns:
3486        tuple: Intersection points of the two circles.
3487    """
3488    # taken from https://stackoverflow.com/questions/55816902/finding-the-
3489    # intersection-of-two-circles
3490    # circle 1: (x0, y0), radius r0
3491    # circle 2: (x1, y1), radius r1
3492
3493    d = sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2)
3494
3495    # non intersecting
3496    if d > r0 + r1:
3497        res = None
3498    # One circle within other
3499    elif d < abs(r0 - r1):
3500        res = None
3501    # coincident circles
3502    elif d == 0 and r0 == r1:
3503        res = None
3504    else:
3505        a = (r0**2 - r1**2 + d**2) / (2 * d)
3506        h = sqrt(r0**2 - a**2)
3507        x2 = x0 + a * (x1 - x0) / d
3508        y2 = y0 + a * (x1 - x0) / d
3509        x3 = x2 + h * (y1 - y0) / d
3510        y3 = y2 - h * (x1 - x0) / d
3511        x4 = x2 - h * (y1 - y0) / d
3512        y4 = y2 + h * (x1 - x0) / d
3513
3514        res = (x3, y3, x4, y4)
3515
3516    return res
3517
3518
3519def circle_segment_intersection(circle, p1, p2):
3520    """Return True if the circle and the line segment intersect.
3521
3522    Args:
3523        circle (Circle): Input circle.
3524        p1 (Point): First point of the line segment.
3525        p2 (Point): Second point of the line segment.
3526
3527    Returns:
3528        bool: True if the circle and the line segment intersect, False otherwise.
3529    """
3530    # if line seg and circle intersects returns true, false otherwise
3531    # c: circle
3532    # p1 and p2 are the endpoints of the line segment
3533
3534    x3, y3 = circle.pos[:2]
3535    x1, y1 = p1[:2]
3536    x2, y2 = p2[:2]
3537    if (
3538        distance(p1, circle.pos) < circle.radius
3539        or distance(p2, circle.pos) < circle.radius
3540    ):
3541        return True
3542    u = ((x3 - x1) * (x2 - x1) + (y3 - y1) * (y2 - y1)) / (
3543        (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)
3544    )
3545    res = False
3546    if 0 <= u <= 1:
3547        x = x1 + u * (x2 - x1)
3548        y = y1 + u * (y2 - y1)
3549        if distance((x, y), circle.pos) < circle.radius:
3550            res = True
3551
3552    return res  # p is not between lp1 and lp2
3553
3554
3555def r_polar(a, b, theta):
3556    """Return the radius (distance between the center and the intersection point)
3557    of the ellipse at the given angle.
3558
3559    Args:
3560        a (float): Semi-major axis of the ellipse.
3561        b (float): Semi-minor axis of the ellipse.
3562        theta (float): Angle in radians.
3563
3564    Returns:
3565        float: Radius of the ellipse at the given angle.
3566    """
3567    return (a * b) / sqrt((b * cos(theta)) ** 2 + (a * sin(theta)) ** 2)
3568
3569
3570def ellipse_line_intersection(a, b, point):
3571    """Return the intersection points of an ellipse and a line segment
3572    connecting the given point to the ellipse center at (0, 0).
3573
3574    Args:
3575        a (float): Semi-major axis of the ellipse.
3576        b (float): Semi-minor axis of the ellipse.
3577        point (Point): Point on the line segment.
3578
3579    Returns:
3580        list[Point]: Intersection points of the ellipse and the line segment.
3581    """
3582    # adapted from http://mathworld.wolfram.com/Ellipse-LineIntersection.html
3583    # a, b is the ellipse width/2 and height/2 and (x_0, y_0) is the point
3584
3585    x_0, y_0 = point[:2]
3586    x = ((a * b) / (sqrt(a**2 * y_0**2 + b**2 * x_0**2))) * x_0
3587    y = ((a * b) / (sqrt(a**2 * y_0**2 + b**2 * x_0**2))) * y_0
3588
3589    return [(x, y), (-x, -y)]
3590
3591
3592def ellipse_tangent(a, b, x, y, tol=0.001):
3593    """Calculates the slope of the tangent line to an ellipse at the point (x, y).
3594    If point is not on the ellipse, return False.
3595
3596    Args:
3597        a (float): Semi-major axis of the ellipse.
3598        b (float): Semi-minor axis of the ellipse.
3599        x (float): x-coordinate of the point.
3600        y (float): y-coordinate of the point.
3601        tol (float, optional): Tolerance. Defaults to 0.001.
3602
3603    Returns:
3604        float: Slope of the tangent line, or False if the point is not on the ellipse.
3605    """
3606    if abs((x**2 / a**2) + (y**2 / b**2) - 1) > tol:
3607        res = False
3608    else:
3609        res = -(b**2 * x) / (a**2 * y)
3610
3611    return res
3612
3613
3614def elliptic_arclength(t_0, t_1, a, b):
3615    """Return the arclength of an ellipse between the given parametric angles.
3616    The ellipse has semi-major axis a and semi-minor axis b.
3617
3618    Args:
3619        t_0 (float): Start parametric angle in radians.
3620        t_1 (float): End parametric angle in radians.
3621        a (float): Semi-major axis of the ellipse.
3622        b (float): Semi-minor axis of the ellipse.
3623
3624    Returns:
3625        float: Arclength of the ellipse between the given parametric angles.
3626    """
3627    # from: https://www.johndcook.com/blog/2022/11/02/elliptic-arc-length/
3628    from scipy.special import ellipeinc # this takes too long to import!!!
3629    m = 1 - (b / a) ** 2
3630    t1 = ellipeinc(t_1 - 0.5 * pi, m)
3631    t0 = ellipeinc(t_0 - 0.5 * pi, m)
3632    return a * (t1 - t0)
3633
3634
3635def central_to_parametric_angle(a, b, phi):
3636    """
3637    Converts a central angle to a parametric angle on an ellipse.
3638
3639    Args:
3640        a (float): Semi-major axis of the ellipse.
3641        b (float): Semi-minor axis of the ellipse.
3642        phi (float): Central angle in radians.
3643
3644    Returns:
3645        float: Parametric angle in radians.
3646    """
3647    t = atan2((a / b) * sin(phi), cos(phi))
3648    if t < 0:
3649        t += 2 * pi
3650
3651    return t
3652
3653
3654def parametric_to_central_angle(a, b, t):
3655    """
3656    Converts a parametric angle on an ellipse to a central angle.
3657
3658    Args:
3659        a (float): Semi-major axis of the ellipse.
3660        b (float): Semi-minor axis of the ellipse.
3661        t (float): Parametric angle in radians.
3662
3663    Returns:
3664        float: Central angle in radians.
3665    """
3666    phi = atan2((b / a) * sin(t), cos(t))
3667    if phi < 0:
3668        phi += 2 * pi
3669
3670    return phi
3671
3672
3673def ellipse_points(center, a, b, n_points):
3674    """Generate points on an ellipse.
3675
3676    Args:
3677        center (tuple): (x, y) coordinates of the ellipse center.
3678        a (float): Length of the semi-major axis.
3679        b (float): Length of the semi-minor axis.
3680        n_points (int): Number of points to generate.
3681
3682    Returns:
3683        np.ndarray: Array of (x, y) coordinates of the ellipse points.
3684    """
3685    t = np.linspace(0, 2 * np.pi, n_points)
3686    x = center[0] + a * np.cos(t)
3687    y = center[1] + b * np.sin(t)
3688
3689    return np.column_stack((x, y))
3690
3691
3692def ellipse_point(a, b, angle):
3693    """Return a point on an ellipse with the given a=width/2, b=height/2, and angle.
3694
3695    Args:
3696        a (float): Semi-major axis of the ellipse.
3697        b (float): Semi-minor axis of the ellipse.
3698        angle (float): Angle in radians.
3699
3700    Returns:
3701        Point: Point on the ellipse.
3702    """
3703    r = r_polar(a, b, angle)
3704
3705    return (r * cos(angle), r * sin(angle))
3706
3707
3708def circle_line_intersection(c, p1, p2):
3709    """Return the intersection points of a circle and a line segment.
3710
3711    Args:
3712        c (Circle): Input circle.
3713        p1 (Point): First point of the line segment.
3714        p2 (Point): Second point of the line segment.
3715
3716    Returns:
3717        tuple: Intersection points of the circle and the line segment.
3718    """
3719
3720    # adapted from http://mathworld.wolfram.com/Circle-LineIntersection.html
3721    # c is the circle and p1 and p2 are the line points
3722    def sgn(num):
3723        if num < 0:
3724            res = -1
3725        else:
3726            res = 1
3727        return res
3728
3729    x1, y1 = p1[:2]
3730    x2, y2 = p2[:2]
3731    r = c.radius
3732    x, y = c.pos[:2]
3733
3734    x1 -= x
3735    x2 -= x
3736    y1 -= y
3737    y2 -= y
3738
3739    dx = x2 - x1
3740    dy = y2 - y1
3741    dr = sqrt(dx**2 + dy**2)
3742    d = x1 * y2 - x2 * y1
3743    d2 = d**2
3744    r2 = r**2
3745    dr2 = dr**2
3746
3747    discriminant = r2 * dr2 - d2
3748
3749    if discriminant > 0:
3750        ddy = d * dy
3751        ddx = d * dx
3752        sqrterm = sqrt(r2 * dr2 - d2)
3753        temp = sgn(dy) * dx * sqrterm
3754
3755        a = (ddy + temp) / dr2
3756        b = (-ddx + abs(dy) * sqrterm) / dr2
3757        if discriminant == 0:
3758            res = (a + x, b + y)
3759        else:
3760            c = (ddy - temp) / dr2
3761            d = (-ddx - abs(dy) * sqrterm) / dr2
3762            res = ((a + x, b + y), (c + x, d + y))
3763
3764    else:
3765        res = False
3766
3767    return res
3768
3769
3770def circle_poly_intersection(circle, polygon):
3771    """Return True if the circle and the polygon intersect.
3772
3773    Args:
3774        circle (Circle): Input circle.
3775        polygon (Polygon): Input polygon.
3776
3777    Returns:
3778        bool: True if the circle and the polygon intersect, False otherwise.
3779    """
3780    points = polygon.vertices
3781    n = len(points)
3782    res = False
3783    for i in range(n):
3784        x = points[i][0]
3785        y = points[i][1]
3786        x1 = points[(i + 1) % n][0]
3787        y1 = points[(i + 1) % n][1]
3788        if circle_segment_intersection(circle, (x, y), (x1, y1)):
3789            res = True
3790            break
3791    return res
3792
3793
3794def point_to_circle_distance(point, center, radius):
3795    """Given a point, center point, and radius, returns distance
3796    between the given point and the circle
3797
3798    Args:
3799        point (Point): Input point.
3800        center (Point): Center of the circle.
3801        radius (float): Radius of the circle.
3802
3803    Returns:
3804        float: Distance between the point and the circle.
3805    """
3806    return abs(distance(center, point) - radius)
3807
3808
3809def get_interior_points(start, end, n_points):
3810    """Given start and end points and number of interior points
3811    returns the positions of the interior points
3812
3813    Args:
3814        start (Point): Start point.
3815        end (Point): End point.
3816        n_points (int): Number of interior points.
3817
3818    Returns:
3819        list[Point]: List of interior points.
3820    """
3821    rot_angle = line_angle(start, end)
3822    length_ = distance(start, end)
3823    seg_length = length_ / (n_points + 1.0)
3824    points = []
3825    for i in range(n_points):
3826        points.append(
3827            rotate_point([start[0] + seg_length * (i + 1), start[1]], start, rot_angle)
3828        )
3829    return points
3830
3831
3832def circle_3point(point1, point2, point3):
3833    """Given three points, returns the center point and radius
3834
3835    Args:
3836        point1 (Point): First point.
3837        point2 (Point): Second point.
3838        point3 (Point): Third point.
3839
3840    Returns:
3841        tuple: Center point and radius of the circle.
3842    """
3843    ax, ay = point1[:2]
3844    bx, by = point2[:2]
3845    cx, cy = point3[:2]
3846    a = bx - ax
3847    b = by - ay
3848    c = cx - ax
3849    d = cy - ay
3850    e = a * (ax + bx) + b * (ay + by)
3851    f = c * (ax + cx) + d * (ay + cy)
3852    g = 2.0 * (a * (cy - by) - b * (cx - bx))
3853    if g == 0:
3854        raise ValueError("Points are collinear!")
3855
3856    px = ((d * e) - (b * f)) / g
3857    py = ((a * f) - (c * e)) / g
3858    r = ((ax - px) ** 2 + (ay - py) ** 2) ** 0.5
3859    return ((px, py), r)
3860
3861
3862def project_point_on_line(point: Vertex, line: Edge):
3863    """Given a point and a line, returns the projection of the point on the line
3864
3865    Args:
3866        point (Vertex): Input point.
3867        line (Edge): Input line.
3868
3869    Returns:
3870        Vertex: Projection of the point on the line.
3871    """
3872    v = point
3873    a, b = line
3874
3875    av = v - a
3876    ab = b - a
3877    t = (av * ab) / (ab * ab)
3878    if t < 0.0:
3879        t = 0.0
3880    elif t > 1.0:
3881        t = 1.0
3882    return a + ab * t
3883
3884
3885class Vertex(list):
3886    """A 3D vertex."""
3887
3888    def __init__(self, x, y, z=0):
3889        self.x = x
3890        self.y = y
3891        self.z = z
3892        self.type = Types.VERTEX
3893        common_properties(self, graphics_object=False)
3894
3895    def __repr__(self):
3896        return f"Vertex({self.x}, {self.y}, {self.z})"
3897
3898    def __eq__(self, other):
3899        return self[0] == other[0] and self[1] == other[1] and self[2] == other[2]
3900
3901    def copy(self):
3902        return Vertex(self.x, self.y, self.z)
3903
3904    def __add__(self, other):
3905        return Vertex(self.x + other.x, self.y + other.y, self.z + other.z)
3906
3907    def __sub__(self, other):
3908        return Vertex(self.x - other.x, self.y - other.y, self.z - other.z)
3909
3910    @property
3911    def coords(self):
3912        """Return the coordinates as a tuple."""
3913        return (self.x, self.y, self.z)
3914
3915    @property
3916    def array(self):
3917        """Homogeneous coordinates as a numpy array."""
3918        return array([self.x, self.y, 1])
3919
3920    def v_tuple(self):
3921        """Return the vertex as a tuple."""
3922        return (self.x, self.y, self.z)
3923
3924    def below(self, other):
3925        """This is for 2D points only
3926
3927        Args:
3928            other (Vertex): Other vertex.
3929
3930        Returns:
3931            bool: True if this vertex is below the other vertex, False otherwise.
3932        """
3933        res = False
3934        if self.y < other.y:
3935            res = True
3936        elif self.y == other.y:
3937            if self.x > other.x:
3938                res = True
3939        return res
3940
3941    def above(self, other):
3942        """This is for 2D points only
3943
3944        Args:
3945            other (Vertex): Other vertex.
3946
3947        Returns:
3948            bool: True if this vertex is above the other vertex, False otherwise.
3949        """
3950        if self.y > other.y:
3951            res = True
3952        elif self.y == other.y and self.x < other.x:
3953            res = True
3954        else:
3955            res = False
3956
3957        return res
3958
3959
3960class Edge:
3961    """A 2D edge."""
3962
3963    def __init__(
3964        self, start_point: Union[Point, Vertex], end_point: Union[Point, Vertex]
3965    ):
3966        if isinstance(start_point, Point):
3967            start = Vertex(*start_point)
3968        elif isinstance(end_point, Vertex):
3969            start = start_point
3970        else:
3971            raise ValueError("Start point should be a Point or Vertex instance.")
3972
3973        if isinstance(end_point, Point):
3974            end = Vertex(*end_point)
3975        elif isinstance(end_point, Vertex):
3976            end = end_point
3977        else:
3978            raise ValueError("End point should be a Point or Vertex instance.")
3979
3980        self.start = start
3981        self.end = end
3982        self.type = Types.EDGE
3983        common_properties(self, graphics_object=False)
3984
3985    def __repr__(self):
3986        return str(f"Edge({self.start}, {self.end})")
3987
3988    def __str__(self):
3989        return str(f"Edge({self.start.point}, {self.end.point})")
3990
3991    def __eq__(self, other):
3992        start = other.start.point
3993        end = other.end.point
3994
3995        return (
3996            isclose(
3997                self.start.point, start, rtol=defaults["rtol"], atol=defaults["atol"]
3998            )
3999            and isclose(
4000                self.end.point, end, rtol=defaults["rtol"], atol=defaults["atol"]
4001            )
4002        ) or (
4003            isclose(self.start.point, end, rtol=defaults["rtol"], atol=defaults["atol"])
4004            and isclose(
4005                self.end.point, start, rtol=defaults["rtol"], atol=defaults["atol"]
4006            )
4007        )
4008
4009    def __getitem__(self, subscript):
4010        vertices = self.vertices
4011        if isinstance(subscript, slice):
4012            res = vertices[subscript.start : subscript.stop : subscript.step]
4013        elif isinstance(subscript, int):
4014            res = vertices[subscript]
4015        else:
4016            raise ValueError("Invalid subscript.")
4017        return res
4018
4019    def __setitem__(self, subscript, value):
4020        vertices = self.vertices
4021        if isinstance(subscript, slice):
4022            vertices[subscript.start : subscript.stop : subscript.step] = value
4023        else:
4024            isinstance(subscript, int)
4025            vertices[subscript] = value
4026
4027    @property
4028    def slope(self):
4029        """Line slope. The slope of the line passing through the start and end points."""
4030        return (self.y2 - self.y1) / (self.x2 - self.x1)
4031
4032    @property
4033    def angle(self):
4034        """Line angle. Angle between the line and the x-axis."""
4035        return atan2(self.y2 - self.y1, self.x2 - self.x1)
4036
4037    @property
4038    def inclination(self):
4039        """Inclination angle. Angle between the line and the x-axis converted to
4040        a value between zero and pi."""
4041        return self.angle % pi
4042
4043    @property
4044    def length(self):
4045        """Length of the line segment."""
4046        return distance(self.start.point, self.end.point)
4047
4048    @property
4049    def x1(self):
4050        """x-coordinate of the start point."""
4051        return self.start.x
4052
4053    @property
4054    def y1(self):
4055        """y-coordinate of the start point."""
4056        return self.start.y
4057
4058    @property
4059    def x2(self):
4060        """x-coordinate of the end point."""
4061        return self.end.x
4062
4063    @property
4064    def y2(self):
4065        """y-coordinate of the end point."""
4066        return self.end.y
4067
4068    @property
4069    def points(self):
4070        """Start and end"""
4071        return [self.start.point, self.end.point]
4072
4073    @property
4074    def vertices(self):
4075        """Start and end vertices."""
4076        return [self.start, self.end]
4077
4078    @property
4079    def array(self):
4080        """Homogeneous coordinates as a numpy array."""
4081        return array([self.start.array, self.end.array])
4082
4083
4084def rotate_point_3D(point: Point, line: Line, angle: float) -> Point:
4085    """Rotate a 2d point (out of paper) about a 2d line by the given angle.
4086    This is used for animating mirror reflections.
4087     Args:
4088         point (Point): Point to rotate.
4089         line (Line): Line to rotate about.
4090         angle (float): Angle of rotation in radians.
4091
4092     Returns:
4093         Point: Rotated point.
4094    """
4095    from ..graphics.affine import rotation_matrix, translation_matrix
4096
4097    p1, p2 = line
4098    line_angle_ = line_angle(p1, p2)
4099    translation = translation_matrix(-p1[0], -p1[1])
4100    rotation = rotation_matrix(-line_angle_, (0, 0))
4101    xform = translation @ rotation
4102    x, y = point
4103    x, y, _ = [x, y, 1] @ xform
4104
4105    y *= cos(angle)
4106
4107    inv_translation = translation_matrix(p1[0], p1[1])
4108    inv_rotation = rotation_matrix(line_angle_, (0, 0))
4109    inv_xform = inv_rotation @ inv_translation
4110    x, y, _ = [x, y, 1] @ inv_xform
4111
4112    return (x, y)
4113
4114
4115def rotate_line_3D(line: Line, about: Line, angle: float) -> Line:
4116    """Rotate a 3d line about a 3d line by the given angle
4117
4118    Args:
4119        line (Line): Line to rotate.
4120        about (Line): Line to rotate about.
4121        angle (float): Angle of rotation in radians.
4122
4123    Returns:
4124        Line: Rotated line.
4125    """
4126    p1 = rotate_point_3D(line[0], about, angle)
4127    p2 = rotate_point_3D(line[1], about, angle)
4128
4129    return [p1, p2]
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]])
@array_function_dispatch(_round_dispatcher)
def around(a, decimals=0, out=None):
3455@array_function_dispatch(_round_dispatcher)
3456def around(a, decimals=0, out=None):
3457    """
3458    Round an array to the given number of decimals.
3459
3460    `around` is an alias of `~numpy.round`.
3461
3462    See Also
3463    --------
3464    ndarray.round : equivalent method
3465    round : alias for this function
3466    ceil, fix, floor, rint, trunc
3467
3468    """
3469    return _wrapfunc(a, 'round', decimals=decimals, out=out)

Round an array to the given number of decimals.

around is an alias of ~numpy.round.

See Also

ndarray.round : equivalent method round : alias for this function ceil, fix, floor, rint, trunc

TWO_PI = 6.283185307179586
def is_number(x: Any) -> bool:
50def is_number(x: Any) -> bool:
51    """
52    Return True if x is a number.
53
54    Args:
55        x (Any): The input value to check.
56
57    Returns:
58        bool: True if x is a number, False otherwise.
59    """
60    return isinstance(x, (int, float, complex)) and not isinstance(x, bool)

Return True if x is a number.

Arguments:
  • x (Any): The input value to check.
Returns:

bool: True if x is a number, False otherwise.

def bbox_overlap( min_x1: float, min_y1: float, max_x2: float, max_y2: float, min_x3: float, min_y3: float, max_x4: float, max_y4: float) -> bool:
63def bbox_overlap(
64    min_x1: float,
65    min_y1: float,
66    max_x2: float,
67    max_y2: float,
68    min_x3: float,
69    min_y3: float,
70    max_x4: float,
71    max_y4: float,
72) -> bool:
73    """
74    Given two bounding boxes, return True if they overlap.
75
76    Args:
77        min_x1 (float): Minimum x-coordinate of the first bounding box.
78        min_y1 (float): Minimum y-coordinate of the first bounding box.
79        max_x2 (float): Maximum x-coordinate of the first bounding box.
80        max_y2 (float): Maximum y-coordinate of the first bounding box.
81        min_x3 (float): Minimum x-coordinate of the second bounding box.
82        min_y3 (float): Minimum y-coordinate of the second bounding box.
83        max_x4 (float): Maximum x-coordinate of the second bounding box.
84        max_y4 (float): Maximum y-coordinate of the second bounding box.
85
86    Returns:
87        bool: True if the bounding boxes overlap, False otherwise.
88    """
89    return not (
90        max_x2 < min_x3 or max_x4 < min_x1 or max_y2 < min_y3 or max_y4 < min_y1
91    )

Given two bounding boxes, return True if they overlap.

Arguments:
  • min_x1 (float): Minimum x-coordinate of the first bounding box.
  • min_y1 (float): Minimum y-coordinate of the first bounding box.
  • max_x2 (float): Maximum x-coordinate of the first bounding box.
  • max_y2 (float): Maximum y-coordinate of the first bounding box.
  • min_x3 (float): Minimum x-coordinate of the second bounding box.
  • min_y3 (float): Minimum y-coordinate of the second bounding box.
  • max_x4 (float): Maximum x-coordinate of the second bounding box.
  • max_y4 (float): Maximum y-coordinate of the second bounding box.
Returns:

bool: True if the bounding boxes overlap, False otherwise.

def sine_wave( amplitude: float, frequency: float, duration: float, sample_rate: float, phase: float = 0) -> "'ndarray'":
 94def sine_wave(
 95    amplitude: float,
 96    frequency: float,
 97    duration: float,
 98    sample_rate: float,
 99    phase: float = 0,
100) -> 'ndarray':
101    """
102    Generate a sine wave.
103
104    Args:
105        amplitude (float): Amplitude of the wave.
106        frequency (float): Frequency of the wave.
107        duration (float): Duration of the wave.
108        sample_rate (float): Sample rate.
109        phase (float, optional): Phase angle of the wave. Defaults to 0.
110
111    Returns:
112        np.ndarray: Time and signal arrays representing the sine wave.
113    """
114    time = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
115    signal = amplitude * np.sin(2 * np.pi * frequency * time + phase)
116    # plt.plot(time, signal)
117    # plt.xlabel('Time (s)')
118    # plt.ylabel('Amplitude')
119    # plt.title('Discretized Sine Wave')
120    # plt.grid(True)
121    # plt.show()
122    return time, signal

Generate a sine wave.

Arguments:
  • amplitude (float): Amplitude of the wave.
  • frequency (float): Frequency of the wave.
  • duration (float): Duration of the wave.
  • sample_rate (float): Sample rate.
  • phase (float, optional): Phase angle of the wave. Defaults to 0.
Returns:

np.ndarray: Time and signal arrays representing the sine wave.

def damping_function(amplitude, duration, sample_rate):
125def damping_function(amplitude, duration, sample_rate):
126    """
127    Generates a damping function based on the given amplitude, duration, and sample rate.
128
129    Args:
130        amplitude (float): The initial amplitude of the damping function.
131        duration (float): The duration over which the damping occurs, in seconds.
132        sample_rate (float): The number of samples per second.
133
134    Returns:
135        list: A list of float values representing the damping function over time.
136    """
137    damping = []
138    for i in range(int(duration * sample_rate)):
139        damping.append(amplitude * exp(-i / (duration * sample_rate)))
140    return damping

Generates a damping function based on the given amplitude, duration, and sample rate.

Arguments:
  • amplitude (float): The initial amplitude of the damping function.
  • duration (float): The duration over which the damping occurs, in seconds.
  • sample_rate (float): The number of samples per second.
Returns:

list: A list of float values representing the damping function over time.

def sine_points( period: float = 40, amplitude: float = 20, duration: float = 40, n_points: int = 100, phase_angle: float = 0, damping: float = 0) -> "'ndarray'":
142def sine_points(
143    period: float = 40,
144    amplitude: float = 20,
145    duration: float = 40,
146    n_points: int = 100,
147    phase_angle: float = 0,
148    damping: float = 0,
149) -> 'ndarray':
150    """
151    Generate sine wave points.
152
153    Args:
154        amplitude (float): Amplitude of the wave.
155        frequency (float): Frequency of the wave.
156        duration (float): Duration of the wave.
157        sample_rate (float): Sample rate.
158        phase (float, optional): Phase angle of the wave. Defaults to 0.
159        damping (float, optional): Damping coefficient. Defaults to 0.
160    Returns:
161        np.ndarray: Array of points representing the sine wave.
162    """
163    phase = phase_angle
164    freq = 1 / period
165    n_cycles = duration / period
166    x = np.linspace(0, duration, int(n_points * n_cycles))
167    y = amplitude * np.sin(2 * np.pi * freq * x + phase)
168    if damping:
169        y *= np.exp(-damping * x)
170    vertices = np.column_stack((x, y)).tolist()
171
172    return vertices

Generate sine wave points.

Arguments:
  • amplitude (float): Amplitude of the wave.
  • frequency (float): Frequency of the wave.
  • duration (float): Duration of the wave.
  • sample_rate (float): Sample rate.
  • phase (float, optional): Phase angle of the wave. Defaults to 0.
  • damping (float, optional): Damping coefficient. Defaults to 0.
Returns:

np.ndarray: Array of points representing the sine wave.

def check_consecutive_duplicates(points, rtol=0, atol=None) -> bool:
175def check_consecutive_duplicates(points, rtol=0, atol=None) -> bool:
176    """Check for consecutive duplicate points in a list of points.
177
178        Args:
179            points (list): List of points to check.
180            rtol (float, optional): Relative tolerance. Defaults to 0.
181            atol (float, optional): Absolute tolerance. Defaults to None.
182
183        Returns:
184            bool: True if consecutive duplicate points are found, False otherwise.
185    """
186    if atol is None:
187        atol = defaults["atol"]
188    if isinstance(points, np.ndarray):
189        points = points.tolist()
190    if points and len(points) > 1:
191        for i, pnt in enumerate(points[:-1]):
192            next_pnt = points[i+1]
193            val1 = pnt[0] + pnt[1]
194            val2 = next_pnt[0] + next_pnt[1]
195            if isclose(val1, val2, rtol=0, atol=atol):
196                if np.allclose(pnt, next_pnt, rtol=0, atol=atol):
197                    return True
198
199    return False

Check for consecutive duplicate points in a list of points.

Arguments:
  • points (list): List of points to check.
  • rtol (float, optional): Relative tolerance. Defaults to 0.
  • atol (float, optional): Absolute tolerance. Defaults to None.
Returns:

bool: True if consecutive duplicate points are found, False otherwise.

def circle_inversion(point, center, radius):
201def circle_inversion(point, center, radius):
202    """
203    Inverts a point with respect to a circle.
204
205    Args:
206        point (tuple): The point to invert, represented as a tuple (x, y).
207        center (tuple): The center of the circle, represented as a tuple (x, y).
208        radius (float): The radius of the circle.
209
210    Returns:
211        tuple: The inverted point, represented as a tuple (x, y).
212    """
213    x, y = point[:2]
214    cx, cy = center[:2]
215    # Calculate the distance from the point to the center of the circle
216    dist = sqrt((x - cx) ** 2 + (y - cy) ** 2)
217    # If the point is at the center of the circle, return the point at infinity
218    if dist == 0:
219        return float("inf"), float("inf")
220    # Calculate the distance from the inverted point to the center of the circle
221    inv_dist = radius**2 / dist
222    # Calculate the inverted point
223    inv_x = cx + inv_dist * (x - cx) / dist
224    inv_y = cy + inv_dist * (y - cy) / dist
225    return inv_x, inv_y

Inverts a point with respect to a circle.

Arguments:
  • point (tuple): The point to invert, represented as a tuple (x, y).
  • center (tuple): The center of the circle, represented as a tuple (x, y).
  • radius (float): The radius of the circle.
Returns:

tuple: The inverted point, represented as a tuple (x, y).

def line_segment_bbox( x1: float, y1: float, x2: float, y2: float) -> tuple[float, float, float, float]:
228def line_segment_bbox(
229    x1: float, y1: float, x2: float, y2: float
230) -> tuple[float, float, float, float]:
231    """
232    Return the bounding box of a line segment.
233
234    Args:
235        x1 (float): Segment start point x-coordinate.
236        y1 (float): Segment start point y-coordinate.
237        x2 (float): Segment end point x-coordinate.
238        y2 (float): Segment end point y-coordinate.
239
240    Returns:
241        tuple: Bounding box as (min_x, min_y, max_x, max_y).
242    """
243    return (min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2))

Return the bounding box of a line segment.

Arguments:
  • x1 (float): Segment start point x-coordinate.
  • y1 (float): Segment start point y-coordinate.
  • x2 (float): Segment end point x-coordinate.
  • y2 (float): Segment end point y-coordinate.
Returns:

tuple: Bounding box as (min_x, min_y, max_x, max_y).

def line_segment_bbox_check(seg1: Sequence[Sequence], seg2: Sequence[Sequence]) -> bool:
246def line_segment_bbox_check(seg1: Line, seg2: Line) -> bool:
247    """
248    Given two line segments, return True if their bounding boxes overlap.
249
250    Args:
251        seg1 (Line): First line segment.
252        seg2 (Line): Second line segment.
253
254    Returns:
255        bool: True if the bounding boxes overlap, False otherwise.
256    """
257    x1, y1 = seg1[0]
258    x2, y2 = seg1[1]
259    x3, y3 = seg2[0]
260    x4, y4 = seg2[1]
261    return bbox_overlap(
262        *line_segment_bbox(x1, y1, x2, y2), *line_segment_bbox(x3, y3, x4, y4)
263    )

Given two line segments, return True if their bounding boxes overlap.

Arguments:
  • seg1 (Line): First line segment.
  • seg2 (Line): Second line segment.
Returns:

bool: True if the bounding boxes overlap, False otherwise.

def all_close_points( points: Sequence[Sequence], dist_tol: float = None, with_dist: bool = False) -> dict[int, list[tuple[typing.Sequence[float], int]]]:
266def all_close_points(
267    points: Sequence[Sequence], dist_tol: float = None, with_dist: bool = False
268) -> dict[int, list[tuple[Point, int]]]:
269    """
270    Find all close points in a list of points along with their ids.
271
272    Args:
273        points (Sequence[Sequence]): List of points with ids [[x1, y1, id1], [x2, y2, id2], ...].
274        dist_tol (float, optional): Distance tolerance. Defaults to None.
275        with_dist (bool, optional): Whether to include distances in the result. Defaults to False.
276
277    Returns:
278        dict: Dictionary of the form {id1: [id2, id3, ...], ...}.
279    """
280    if dist_tol is None:
281        dist_tol = defaults["dist_tol"]
282    point_arr = np.array(points, dtype=np.float32)  # points array [[x1, y1, id1], ...]]
283    n_rows = len(points)
284    point_arr = point_arr[point_arr[:, 0].argsort()]  # sort by x values in the
285    # first column
286    xmin = point_arr[:, 0] - dist_tol * 2
287    xmin = xmin.reshape(n_rows, 1)
288    xmax = point_arr[:, 0] + dist_tol * 2
289    xmax = xmax.reshape(n_rows, 1)
290    point_arr = np.concatenate((point_arr, xmin, xmax), 1)  # [x, y, id, xmin, xmax]
291
292    i_id, i_xmin, i_xmax = 2, 3, 4  # column indices
293    d_connections = {}
294    for i in range(n_rows):
295        d_connections[int(point_arr[i, 2])] = []
296    pairs = []
297    dist_tol2 = dist_tol * dist_tol
298    for i in range(n_rows):
299        x, y, id1, sl_xmin, sl_xmax = point_arr[i, :]
300        id1 = int(id1)
301        point = (x, y)
302        start = i + 1
303        candidates = point_arr[start:, :][
304            (
305                (point_arr[start:, i_xmax] >= sl_xmin)
306                & (point_arr[start:, i_xmin] <= sl_xmax)
307            )
308        ]
309        for cand in candidates:
310            id2 = int(cand[i_id])
311            point2 = cand[:2]
312            if close_points2(point, point2, dist2=dist_tol2):
313                d_connections[id1].append(id2)
314                d_connections[id2].append(id1)
315                if with_dist:
316                    pairs.append((id1, id2, distance(point, point2)))
317                else:
318                    pairs.append((id1, id2))
319    res = {}
320    for k, v in d_connections.items():
321        if v:
322            res[k] = v
323    return res, pairs

Find all close points in a list of points along with their ids.

Arguments:
  • points (Sequence[Sequence]): List of points with ids [[x1, y1, id1], [x2, y2, id2], ...].
  • dist_tol (float, optional): Distance tolerance. Defaults to None.
  • with_dist (bool, optional): Whether to include distances in the result. Defaults to False.
Returns:

dict: Dictionary of the form {id1: [id2, id3, ...], ...}.

def is_simple(polygon, rtol: float = None, atol: float = None) -> bool:
326def is_simple(
327    polygon,
328    rtol: float = None,
329    atol: float = None,
330) -> bool:
331    """
332    Return True if the polygon is simple.
333
334    Args:
335        polygon (list): List of points representing the polygon.
336        rtol (float, optional): Relative tolerance. Defaults to None.
337        atol (float, optional): Absolute tolerance. Defaults to None.
338
339    Returns:
340        bool: True if the polygon is simple, False otherwise.
341    """
342    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
343
344    if not close_points2(polygon[0], polygon[-1]):
345        polygon.append(polygon[0])
346    segments = [[polygon[i], polygon[i + 1]] for i in range(len(polygon) - 1)]
347
348    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
349    segment_coords = []
350    for segment in segments:
351        segment_coords.append(
352            [segment[0][0], segment[0][1], segment[1][0], segment[1][1]]
353        )
354    seg_arr = np.array(segment_coords)  # segments array
355    n_rows = seg_arr.shape[0]
356    xmin = np.minimum(seg_arr[:, 0], seg_arr[:, 2]).reshape(n_rows, 1)
357    xmax = np.maximum(seg_arr[:, 0], seg_arr[:, 2]).reshape(n_rows, 1)
358    ymin = np.minimum(seg_arr[:, 1], np.maximum(seg_arr[:, 3])).reshape(n_rows, 1)
359    ymax = np.maximum(seg_arr[:, 1], seg_arr[:, 3]).reshape(n_rows, 1)
360    id_ = np.arange(n_rows).reshape(n_rows, 1)
361    seg_arr = np.concatenate((seg_arr, xmin, ymin, xmax, ymax, id_), 1)
362    seg_arr = seg_arr[seg_arr[:, 4].argsort()]
363    i_xmin, i_ymin, i_xmax, i_ymax, i_id = range(4, 9)  # column indices
364
365    s_processed = set()  # set of processed segment pairs
366    for i in range(n_rows):
367        x1, y1, x2, y2, sl_xmin, sl_ymin, sl_xmax, sl_ymax, id1 = seg_arr[i, :]
368        id1 = int(id1)
369        segment = [x1, y1, x2, y2]
370        start = i + 1  # keep pushing the sweep line forward
371        candidates = seg_arr[start:, :][
372            (
373                (
374                    (seg_arr[start:, i_xmax] >= sl_xmin)
375                    & (seg_arr[start:, i_xmin] <= sl_xmax)
376                )
377                & (
378                    (seg_arr[start:, i_ymax] >= sl_ymin)
379                    & (seg_arr[start:, i_ymin] <= sl_ymax)
380                )
381            )
382        ]
383        for cand in candidates:
384            id2 = int(cand[i_id])
385            pair = frozenset((id1, id2))
386            if pair in s_processed:
387                continue
388            s_processed.add(pair)
389            seg2 = cand[:4]
390            x1, y1, x2, y2 = segment
391            x3, y3, x4, y4 = seg2
392            res = intersection3(x1, y1, x2, y2, x3, y3, x4, y4)
393            if res[0] == Connection.COLL_CHAIN:
394                length1 = distance((x1, y1), (x2, y2))
395                length2 = distance((x3, y3), (x4, y4))
396                p1, p2 = res[1][0], res[1][2]
397                chain_length = distance(p1, p2)
398                if not isclose(length1 + length2, chain_length, rtol=rtol, atol=atol):
399                    return False
400                else:
401                    continue
402            if res[0] in (Connection.CHAIN, Connection.PARALLEL):
403                continue
404            if res[0] != Connection.DISJOINT:
405                return False
406
407    return True

Return True if the polygon is simple.

Arguments:
  • polygon (list): List of points representing the polygon.
  • rtol (float, optional): Relative tolerance. Defaults to None.
  • atol (float, optional): Absolute tolerance. Defaults to None.
Returns:

bool: True if the polygon is simple, False otherwise.

def all_intersections( segments: Sequence[Sequence[Sequence]], rtol: float = None, atol: float = None, use_intersection3: bool = False) -> dict[int, list[tuple[typing.Sequence[float], int]]]:
410def all_intersections(
411    segments: Sequence[Line],
412    rtol: float = None,
413    atol: float = None,
414    use_intersection3: bool = False,
415) -> dict[int, list[tuple[Point, int]]]:
416    """
417    Find all intersection points of the given list of segments
418    (sweep line algorithm variant)
419
420    Args:
421        segments (Sequence[Line]): List of line segments [[[x1, y1], [x2, y2]], [[x1, y1], [x2, y2]], ...].
422        rtol (float, optional): Relative tolerance. Defaults to None.
423        atol (float, optional): Absolute tolerance. Defaults to None.
424        use_intersection3 (bool, optional): Whether to use intersection3 function. Defaults to False.
425
426    Returns:
427        dict: Dictionary of the form {segment_id: [[id1, (x1, y1)], [id2, (x2, y2)]], ...}.
428    """
429    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
430    segment_coords = []
431    for segment in segments:
432        segment_coords.append(
433            [segment[0][0], segment[0][1], segment[1][0], segment[1][1]]
434        )
435    seg_arr = np.array(segment_coords)  # segments array
436    n_rows = seg_arr.shape[0]
437    xmin = np.minimum(seg_arr[:, 0], seg_arr[:, 2]).reshape(n_rows, 1)
438    xmax = np.maximum(seg_arr[:, 0], seg_arr[:, 2]).reshape(n_rows, 1)
439    ymin = np.minimum(seg_arr[:, 1], seg_arr[:, 3]).reshape(n_rows, 1)
440    ymax = np.maximum(seg_arr[:, 1], seg_arr[:, 3]).reshape(n_rows, 1)
441    id_ = np.arange(n_rows).reshape(n_rows, 1)
442    seg_arr = np.concatenate((seg_arr, xmin, ymin, xmax, ymax, id_), 1)
443    seg_arr = seg_arr[seg_arr[:, 4].argsort()]
444    i_xmin, i_ymin, i_xmax, i_ymax, i_id = range(4, 9)  # column indices
445    # ind1, ind2 are indexes of segments in the list of segments
446    d_ind1_x_point_ind2 = {}  # {id1: [((x, y), id2), ...], ...}
447    d_ind1_conn_type_x_res_ind2 = {}  # {id1: [(conn_type, x_res, id2), ...], ...}
448    for i in range(n_rows):
449        if use_intersection3:
450            d_ind1_conn_type_x_res_ind2[i] = []
451        else:
452            d_ind1_x_point_ind2[i] = []
453    x_points = []  # intersection points
454    s_processed = set()  # set of processed segment pairs
455    for i in range(n_rows):
456        x1, y1, x2, y2, sl_xmin, sl_ymin, sl_xmax, sl_ymax, id1 = seg_arr[i, :]
457        id1 = int(id1)
458        segment = [x1, y1, x2, y2]
459        start = i + 1  # keep pushing the sweep line forward
460        # filter by overlap of the bounding boxes of the segments with the
461        # sweep line's active segment. If the bounding boxes do not overlap,
462        # the segments cannot intersect. If the bounding boxes overlap,
463        # the segments may intersect.
464        candidates = seg_arr[start:, :][
465            (
466                (
467                    (seg_arr[start:, i_xmax] >= sl_xmin)
468                    & (seg_arr[start:, i_xmin] <= sl_xmax)
469                )
470                & (
471                    (seg_arr[start:, i_ymax] >= sl_ymin)
472                    & (seg_arr[start:, i_ymin] <= sl_ymax)
473                )
474            )
475        ]
476        for cand in candidates:
477            id2 = int(cand[i_id])
478            pair = frozenset((id1, id2))
479            if pair in s_processed:
480                continue
481            s_processed.add(pair)
482            seg2 = cand[:4]
483            if use_intersection3:
484                # connection type, point/segment
485                res = intersection3(*segment, *seg2, rtol, atol)
486                conn_type, x_res = res  # x_res can be a segment or a point
487            else:
488                # connection type, point
489                res = intersection2(*segment, *seg2, rtol, atol)
490                conn_type, x_point = res
491            if use_intersection3:
492                if conn_type not in [Connection.DISJOINT, Connection.PARALLEL]:
493                    d_ind1_conn_type_x_res_ind2[id1].append((conn_type, x_res, id2))
494                    d_ind1_conn_type_x_res_ind2[id2].append((conn_type, x_res, id1))
495            else:
496                if conn_type == Connection.INTERSECT:
497                    d_ind1_x_point_ind2[id1].append((x_point, id2))
498                    d_ind1_x_point_ind2[id2].append((x_point, id1))
499                    x_points.append(res[1])
500
501    d_results = {}
502    if use_intersection3:
503        for k, v in d_ind1_conn_type_x_res_ind2.items():
504            if v:
505                d_results[k] = v
506        res = d_results
507    else:
508        for k, v in d_ind1_x_point_ind2.items():
509            if v:
510                d_results[k] = v
511        res = d_results, x_points
512
513    return res

Find all intersection points of the given list of segments (sweep line algorithm variant)

Arguments:
  • segments (Sequence[Line]): List of line segments [[[x1, y1], [x2, y2]], [[x1, y1], [x2, y2]], ...].
  • rtol (float, optional): Relative tolerance. Defaults to None.
  • atol (float, optional): Absolute tolerance. Defaults to None.
  • use_intersection3 (bool, optional): Whether to use intersection3 function. Defaults to False.
Returns:

dict: Dictionary of the form {segment_id: [[id1, (x1, y1)], [id2, (x2, y2)]], ...}.

def dot_product2(a: Sequence[float], b: Sequence[float], c: Sequence[float]) -> float:
516def dot_product2(a: Point, b: Point, c: Point) -> float:
517    """Dot product of two vectors. AB and BC
518    Args:
519        a (Point): First point, creating vector BA
520        b (Point): Second point, common point for both vectors
521        c (Point): Third point, creating vector BC
522
523    Returns:
524        float: The dot product of vectors BA and BC
525    Note:
526        The function calculates (a-b)·(c-b) which is the dot product of vectors BA and BC.
527        This is useful for finding angles between segments that share a common point.
528    """
529    a_x, a_y = a[:2]
530    b_x, b_y = b[:2]
531    c_x, c_y = c[:2]
532    b_a_x = a_x - b_x
533    b_a_y = a_y - b_y
534    b_c_x = c_x - b_x
535    b_c_y = c_y - b_y
536    return b_a_x * b_c_x + b_a_y * b_c_y

Dot product of two vectors. AB and BC

Arguments:
  • a (Point): First point, creating vector BA
  • b (Point): Second point, common point for both vectors
  • c (Point): Third point, creating vector BC
Returns:

float: The dot product of vectors BA and BC

Note:

The function calculates (a-b)·(c-b) which is the dot product of vectors BA and BC. This is useful for finding angles between segments that share a common point.

def cross_product2(a: Sequence[float], b: Sequence[float], c: Sequence[float]) -> float:
539def cross_product2(a: Point, b: Point, c: Point) -> float:
540    """
541    Return the cross product of two vectors: BA and BC.
542
543    Args:
544        a (Point): First point, creating vector BA
545        b (Point): Second point, common point for both vectors
546        c (Point): Third point, creating vector BC
547
548    Returns:
549        float: The z-component of cross product between vectors BA and BC
550
551    Note:
552        This gives the signed area of the parallelogram formed by the vectors BA and BC.
553        The sign indicates the orientation (positive for counter-clockwise, negative for clockwise).
554        It is useful for determining the orientation of three points and calculating angles.
555
556    vec1 = b - a
557    vec2 = c - b
558    """
559    a_x, a_y = a[:2]
560    b_x, b_y = b[:2]
561    c_x, c_y = c[:2]
562    b_a_x = a_x - b_x
563    b_a_y = a_y - b_y
564    b_c_x = c_x - b_x
565    b_c_y = c_y - b_y
566    return b_a_x * b_c_y - b_a_y * b_c_x

Return the cross product of two vectors: BA and BC.

Arguments:
  • a (Point): First point, creating vector BA
  • b (Point): Second point, common point for both vectors
  • c (Point): Third point, creating vector BC
Returns:

float: The z-component of cross product between vectors BA and BC

Note:

This gives the signed area of the parallelogram formed by the vectors BA and BC. The sign indicates the orientation (positive for counter-clockwise, negative for clockwise). It is useful for determining the orientation of three points and calculating angles.

vec1 = b - a vec2 = c - b

def angle_between_lines2( point1: Sequence[float], point2: Sequence[float], point3: Sequence[float]) -> float:
569def angle_between_lines2(point1: Point, point2: Point, point3: Point) -> float:
570    """
571    Given line1 as point1 and point2, and line2 as point2 and point3
572    return the angle between two lines
573    (point2 is the corner point)
574
575    Args:
576        point1 (Point): First point of the first line.
577        point2 (Point): Second point of the first line and first point of the second line.
578        point3 (Point): Second point of the second line.
579
580    Returns:
581        float: Angle between the two lines in radians.
582    """
583    return atan2(
584        cross_product2(point1, point2, point3), dot_product2(point1, point2, point3)
585    )

Given line1 as point1 and point2, and line2 as point2 and point3 return the angle between two lines (point2 is the corner point)

Arguments:
  • point1 (Point): First point of the first line.
  • point2 (Point): Second point of the first line and first point of the second line.
  • point3 (Point): Second point of the second line.
Returns:

float: Angle between the two lines in radians.

def angled_line(line: Sequence[Sequence], theta: float) -> Sequence[Sequence]:
588def angled_line(line: Line, theta: float) -> Line:
589    """
590    Given a line find another line with theta radians between them.
591
592    Args:
593        line (Line): Input line.
594        theta (float): Angle in radians.
595
596    Returns:
597        Line: New line with the given angle.
598    """
599    # find the angle of the line
600    x1, y1 = line[0]
601    x2, y2 = line[1]
602    theta1 = atan2(y2 - y1, x2 - x1)
603    theta2 = theta1 + theta
604    # find the length of the line
605    dx = x2 - x1
606    dy = y2 - y1
607    length_ = (dx**2 + dy**2) ** 0.5
608    # find the new line
609    x3 = x1 + length_ * cos(theta2)
610    y3 = y1 + length_ * sin(theta2)
611
612    return [(x1, y1), (x3, y3)]

Given a line find another line with theta radians between them.

Arguments:
  • line (Line): Input line.
  • theta (float): Angle in radians.
Returns:

Line: New line with the given angle.

def angled_vector(angle: float) -> Sequence[float]:
615def angled_vector(angle: float) -> Sequence[float]:
616    """
617    Return a vector with the given angle
618
619    Args:
620        angle (float): Angle in radians.
621
622    Returns:
623        Sequence[float]: Vector with the given angle.
624    """
625    return [cos(angle), sin(angle)]

Return a vector with the given angle

Arguments:
  • angle (float): Angle in radians.
Returns:

Sequence[float]: Vector with the given angle.

def close_points2(p1: Sequence[float], p2: Sequence[float], dist2: float = 0.01) -> bool:
628def close_points2(p1: Point, p2: Point, dist2: float = 0.01) -> bool:
629    """
630    Return True if two points are close to each other.
631
632    Args:
633        p1 (Point): First point.
634        p2 (Point): Second point.
635        dist2 (float, optional): Square of the threshold distance. Defaults to 0.01.
636
637    Returns:
638        bool: True if the points are close to each other, False otherwise.
639    """
640    return distance2(p1, p2) <= dist2

Return True if two points are close to each other.

Arguments:
  • p1 (Point): First point.
  • p2 (Point): Second point.
  • dist2 (float, optional): Square of the threshold distance. Defaults to 0.01.
Returns:

bool: True if the points are close to each other, False otherwise.

def close_angles(angle1: float, angle2: float, angtol=None) -> bool:
643def close_angles(angle1: float, angle2: float, angtol=None) -> bool:
644    """
645    Return True if two angles are close to each other.
646
647    Args:
648        angle1 (float): First angle in radians.
649        angle2 (float): Second angle in radians.
650        angtol (float, optional): Angle tolerance. Defaults to None.
651
652    Returns:
653        bool: True if the angles are close to each other, False otherwise.
654    """
655    if angtol is None:
656        angtol = defaults["angtol"]
657
658    return (abs(angle1 - angle2) % (2 * pi)) < angtol

Return True if two angles are close to each other.

Arguments:
  • angle1 (float): First angle in radians.
  • angle2 (float): Second angle in radians.
  • angtol (float, optional): Angle tolerance. Defaults to None.
Returns:

bool: True if the angles are close to each other, False otherwise.

def distance(p1: Sequence[float], p2: Sequence[float]) -> float:
661def distance(p1: Point, p2: Point) -> float:
662    """
663    Return the distance between two points.
664
665    Args:
666        p1 (Point): First point.
667        p2 (Point): Second point.
668
669    Returns:
670        float: Distance between the two points.
671    """
672    return hypot(p2[0] - p1[0], p2[1] - p1[1])

Return the distance between two points.

Arguments:
  • p1 (Point): First point.
  • p2 (Point): Second point.
Returns:

float: Distance between the two points.

def distance2(p1: Sequence[float], p2: Sequence[float]) -> float:
675def distance2(p1: Point, p2: Point) -> float:
676    """
677    Return the squared distance between two points.
678    Useful for comparing distances without the need to
679    compute the square root.
680
681    Args:
682        p1 (Point): First point.
683        p2 (Point): Second point.
684
685    Returns:
686        float: Squared distance between the two points.
687    """
688    return (p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2

Return the squared distance between two points. Useful for comparing distances without the need to compute the square root.

Arguments:
  • p1 (Point): First point.
  • p2 (Point): Second point.
Returns:

float: Squared distance between the two points.

def connect2( poly_point1: list[typing.Sequence[float]], poly_point2: list[typing.Sequence[float]], dist_tol: float = None, rtol: float = None) -> list[typing.Sequence[float]]:
691def connect2(
692    poly_point1: list[Point],
693    poly_point2: list[Point],
694    dist_tol: float = None,
695    rtol: float = None,
696) -> list[Point]:
697    """
698    Connect two polypoints together.
699
700    Args:
701        poly_point1 (list[Point]): First list of points.
702        poly_point2 (list[Point]): Second list of points.
703        dist_tol (float, optional): Distance tolerance. Defaults to None.
704        rtol (float, optional): Relative tolerance. Defaults to None.
705
706    Returns:
707        list[Point]: Connected list of points.
708    """
709    rtol, dist_tol = get_defaults(["rtol", "dist_tol"], [rtol, dist_tol])
710    dist_tol2 = dist_tol * dist_tol
711    start1, end1 = poly_point1[0], poly_point1[-1]
712    start2, end2 = poly_point2[0], poly_point2[-1]
713    pp1 = poly_point1[:]
714    pp2 = poly_point2[:]
715    points = []
716    if close_points2(end1, start2, dist2=dist_tol2):
717        points.extend(pp1)
718        points.extend(pp2[1:])
719    elif close_points2(end1, end2, dist2=dist_tol2):
720        points.extend(pp1)
721        pp2.reverse()
722        points.extend(pp2[1:])
723    elif close_points2(start1, start2, dist2=dist_tol2):
724        pp1.reverse()
725        points.extend(pp1)
726        points.extend(pp2[1:])
727    elif close_points2(start1, end2, dist2=dist_tol2):
728        pp1.reverse()
729        points.extend(pp1)
730        pp2.reverse()
731        points.extend(pp2[1:])
732
733    return points

Connect two polypoints together.

Arguments:
  • poly_point1 (list[Point]): First list of points.
  • poly_point2 (list[Point]): Second list of points.
  • dist_tol (float, optional): Distance tolerance. Defaults to None.
  • rtol (float, optional): Relative tolerance. Defaults to None.
Returns:

list[Point]: Connected list of points.

def stitch( lines: list[typing.Sequence[typing.Sequence]], closed: bool = True, return_points: bool = True, rtol: float = None, atol: float = None) -> list[typing.Sequence[float]]:
736def stitch(
737    lines: list[Line],
738    closed: bool = True,
739    return_points: bool = True,
740    rtol: float = None,
741    atol: float = None,
742) -> list[Point]:
743    """
744    Stitches a list of lines together.
745
746    Args:
747        lines (list[Line]): List of lines to stitch.
748        closed (bool, optional): Whether the lines form a closed shape. Defaults to True.
749        return_points (bool, optional): Whether to return points or lines. Defaults to True.
750        rtol (float, optional): Relative tolerance. Defaults to None.
751        atol (float, optional): Absolute tolerance. Defaults to None.
752
753    Returns:
754        list[Point]: Stitched list of points or lines.
755    """
756    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
757    if closed:
758        points = []
759    else:
760        points = [lines[0][0]]
761    for i, line in enumerate(lines[:-1]):
762        x1, y1 = line[0]
763        x2, y2 = line[1]
764        x3, y3 = lines[i + 1][0]
765        x4, y4 = lines[i + 1][1]
766        x_point = intersect2(x1, y1, x2, y2, x3, y3, x4, y4)
767        if x_point:
768            points.append(x_point)
769    if closed:
770        x1, y1 = lines[-1][0]
771        x2, y2 = lines[-1][1]
772        x3, y3 = lines[0][0]
773        x4, y4 = lines[0][1]
774        final_x = intersect2(
775            x1,
776            y1,
777            x2,
778            y2,
779            x3,
780            y3,
781            x4,
782            y4,
783        )
784        if final_x:
785            points.insert(0, final_x)
786            points.append(final_x)
787    else:
788        points.append(lines[-1][1])
789    if return_points:
790        res = points
791    else:
792        res = connected_pairs(points)
793
794    return res

Stitches a list of lines together.

Arguments:
  • lines (list[Line]): List of lines to stitch.
  • closed (bool, optional): Whether the lines form a closed shape. Defaults to True.
  • return_points (bool, optional): Whether to return points or lines. Defaults to True.
  • rtol (float, optional): Relative tolerance. Defaults to None.
  • atol (float, optional): Absolute tolerance. Defaults to None.
Returns:

list[Point]: Stitched list of points or lines.

def double_offset_polylines( lines: list[typing.Sequence[float]], offset: float = 1, rtol: float = None, atol: float = None) -> list[typing.Sequence[float]]:
797def double_offset_polylines(
798    lines: list[Point], offset: float = 1, rtol: float = None, atol: float = None
799) -> list[Point]:
800    """
801    Return a list of double offset lines from a list of lines.
802
803    Args:
804        lines (list[Point]): List of points representing the lines.
805        offset (float, optional): Offset distance. Defaults to 1.
806        rtol (float, optional): Relative tolerance. Defaults to None.
807        atol (float, optional): Absolute tolerance. Defaults to None.
808
809    Returns:
810        list[Point]: List of double offset lines.
811    """
812    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
813    lines1 = []
814    lines2 = []
815    for i, point in enumerate(lines[:-1]):
816        line = [point, lines[i + 1]]
817        line1, line2 = double_offset_lines(line, offset)
818        lines1.append(line1)
819        lines2.append(line2)
820    lines1 = stitch(lines1, closed=False)
821    lines2 = stitch(lines2, closed=False)
822    return [lines1, lines2]

Return a list of double offset lines from a list of lines.

Arguments:
  • lines (list[Point]): List of points representing the lines.
  • offset (float, optional): Offset distance. Defaults to 1.
  • rtol (float, optional): Relative tolerance. Defaults to None.
  • atol (float, optional): Absolute tolerance. Defaults to None.
Returns:

list[Point]: List of double offset lines.

def polygon_cg(points: list[typing.Sequence[float]]) -> Sequence[float]:
825def polygon_cg(points: list[Point]) -> Point:
826    """
827    Given a list of points that define a polygon, return the center point.
828
829    Args:
830        points (list[Point]): List of points representing the polygon.
831
832    Returns:
833        Point: Center point of the polygon.
834    """
835    cx = cy = 0
836    n_points = len(points)
837    for i in range(n_points):
838        x = points[i][0]
839        y = points[i][1]
840        xnext = points[(i + 1) % n_points][0]
841        ynext = points[(i + 1) % n_points][1]
842
843        temp = x * ynext - xnext * y
844        cx += (x + xnext) * temp
845        cy += (y + ynext) * temp
846    area_ = polygon_area(points)
847    denom = area_ * 6
848    if denom:
849        res = [cx / denom, cy / denom]
850    else:
851        res = None
852    return res

Given a list of points that define a polygon, return the center point.

Arguments:
  • points (list[Point]): List of points representing the polygon.
Returns:

Point: Center point of the polygon.

def polygon_center2(polygon_points: list[typing.Sequence[float]]) -> Sequence[float]:
855def polygon_center2(polygon_points: list[Point]) -> Point:
856    """
857    Given a list of points that define a polygon, return the center point.
858
859    Args:
860        polygon_points (list[Point]): List of points representing the polygon.
861
862    Returns:
863        Point: Center point of the polygon.
864    """
865    n = len(polygon_points)
866    x = 0
867    y = 0
868    for point in polygon_points:
869        x += point[0]
870        y += point[1]
871    x = x / n
872    y = y / n
873    return [x, y]

Given a list of points that define a polygon, return the center point.

Arguments:
  • polygon_points (list[Point]): List of points representing the polygon.
Returns:

Point: Center point of the polygon.

def polygon_center(polygon_points: list[typing.Sequence[float]]) -> Sequence[float]:
876def polygon_center(polygon_points: list[Point]) -> Point:
877    """
878    Given a list of points that define a polygon, return the center point.
879
880    Args:
881        polygon_points (list[Point]): List of points representing the polygon.
882
883    Returns:
884        Point: Center point of the polygon.
885    """
886    x = 0
887    y = 0
888    for i, point in enumerate(polygon_points[:-1]):
889        x += point[0] * (polygon_points[i - 1][1] - polygon_points[i + 1][1])
890        y += point[1] * (polygon_points[i - 1][0] - polygon_points[i + 1][0])
891    area_ = polygon_area(polygon_points)
892    return (x / (6 * area_), y / (6 * area_))

Given a list of points that define a polygon, return the center point.

Arguments:
  • polygon_points (list[Point]): List of points representing the polygon.
Returns:

Point: Center point of the polygon.

def offset_polygon( polygon: list[typing.Sequence[float]], offset: float = -1, dist_tol: float = None) -> list[typing.Sequence[float]]:
895def offset_polygon(
896    polygon: list[Point], offset: float = -1, dist_tol: float = None
897) -> list[Point]:
898    """
899    Return a list of offset lines from a list of lines.
900
901    Args:
902        polygon (list[Point]): List of points representing the polygon.
903        offset (float, optional): Offset distance. Defaults to -1.
904        dist_tol (float, optional): Distance tolerance. Defaults to None.
905
906    Returns:
907        list[Point]: List of offset lines.
908    """
909    if dist_tol is None:
910        dist_tol = defaults["dist_tol"]
911    polygon = list(polygon[:])
912    dist_tol2 = dist_tol * dist_tol
913    if not right_handed(polygon):
914        polygon.reverse()
915    if not close_points2(polygon[0], polygon[-1], dist2=dist_tol2):
916        polygon.append(polygon[0])
917    poly = []
918    for i, point in enumerate(polygon[:-1]):
919        line = [point, polygon[i + 1]]
920        offset_edge = offset_line(line, -offset)
921        poly.append(offset_edge)
922
923    poly = stitch(poly, closed=True)
924    return poly

Return a list of offset lines from a list of lines.

Arguments:
  • polygon (list[Point]): List of points representing the polygon.
  • offset (float, optional): Offset distance. Defaults to -1.
  • dist_tol (float, optional): Distance tolerance. Defaults to None.
Returns:

list[Point]: List of offset lines.

def double_offset_polygons( polygon: list[typing.Sequence[float]], offset: float = 1, dist_tol: float = None, **kwargs) -> list[typing.Sequence[float]]:
927def double_offset_polygons(
928    polygon: list[Point], offset: float = 1, dist_tol: float = None, **kwargs
929) -> list[Point]:
930    """
931    Return a list of double offset lines from a list of lines.
932
933    Args:
934        polygon (list[Point]): List of points representing the polygon.
935        offset (float, optional): Offset distance. Defaults to 1.
936        dist_tol (float, optional): Distance tolerance. Defaults to None.
937
938    Returns:
939        list[Point]: List of double offset lines.
940    """
941    if dist_tol is None:
942        dist_tol = defaults["dist_tol"]
943    if not right_handed(polygon):
944        polygon.reverse()
945    poly1 = []
946    poly2 = []
947    for i, point in enumerate(polygon[:-1]):
948        line = [point, polygon[i + 1]]
949        line1, line2 = double_offset_lines(line, offset)
950        poly1.append(line1)
951        poly2.append(line2)
952    poly1 = stitch(poly1)
953    poly2 = stitch(poly2)
954    if "canvas" in kwargs:
955        canvas = kwargs["canvas"]
956        if canvas:
957            canvas.new_page()
958            from ..graphics.shape import Shape
959
960            closed = close_points2(poly1[0], poly1[-1])
961            canvas.draw(Shape(poly1, closed=closed), fill=False)
962            closed = close_points2(poly2[0], poly2[-1])
963            canvas.draw(Shape(poly2, closed=closed), fill=False)
964    return [poly1, poly2]

Return a list of double offset lines from a list of lines.

Arguments:
  • polygon (list[Point]): List of points representing the polygon.
  • offset (float, optional): Offset distance. Defaults to 1.
  • dist_tol (float, optional): Distance tolerance. Defaults to None.
Returns:

list[Point]: List of double offset lines.

def offset_polygon_points( polygon: list[typing.Sequence[float]], offset: float = 1, dist_tol: float = None) -> list[typing.Sequence[float]]:
967def offset_polygon_points(
968    polygon: list[Point], offset: float = 1, dist_tol: float = None
969) -> list[Point]:
970    """
971    Return a list of double offset lines from a list of lines.
972
973    Args:
974        polygon (list[Point]): List of points representing the polygon.
975        offset (float, optional): Offset distance. Defaults to 1.
976        dist_tol (float, optional): Distance tolerance. Defaults to None.
977
978    Returns:
979        list[Point]: List of double offset lines.
980    """
981    if dist_tol is None:
982        dist_tol = defaults["dist_tol"]
983    dist_tol2 = dist_tol * dist_tol
984    polygon = list(polygon)
985    if not close_points2(polygon[0], polygon[-1], dist2=dist_tol2):
986        polygon.append(polygon[0])
987    poly = []
988    for i, point in enumerate(polygon[:-1]):
989        line = [point, polygon[i + 1]]
990        offset_edge = offset_line(line, offset)
991        poly.append(offset_edge)
992
993    poly = stitch(poly)
994    if not right_handed(poly):
995        poly.reverse()
996    return poly

Return a list of double offset lines from a list of lines.

Arguments:
  • polygon (list[Point]): List of points representing the polygon.
  • offset (float, optional): Offset distance. Defaults to 1.
  • dist_tol (float, optional): Distance tolerance. Defaults to None.
Returns:

list[Point]: List of double offset lines.

def double_offset_lines( line: Sequence[Sequence], offset: float = 1) -> tuple[typing.Sequence[typing.Sequence], typing.Sequence[typing.Sequence]]:
 999def double_offset_lines(line: Line, offset: float = 1) -> tuple[Line, Line]:
1000    """
1001    Return two offset lines to a given line segment with the given offset amount.
1002
1003    Args:
1004        line (Line): Input line segment.
1005        offset (float, optional): Offset distance. Defaults to 1.
1006
1007    Returns:
1008        tuple[Line, Line]: Two offset lines.
1009    """
1010    line1 = offset_line(line, offset)
1011    line2 = offset_line(line, -offset)
1012
1013    return line1, line2

Return two offset lines to a given line segment with the given offset amount.

Arguments:
  • line (Line): Input line segment.
  • offset (float, optional): Offset distance. Defaults to 1.
Returns:

tuple[Line, Line]: Two offset lines.

def equal_lines( line1: Sequence[Sequence], line2: Sequence[Sequence], dist_tol: float = None) -> bool:
1016def equal_lines(line1: Line, line2: Line, dist_tol: float = None) -> bool:
1017    """
1018    Return True if two lines are close enough.
1019
1020    Args:
1021        line1 (Line): First line.
1022        line2 (Line): Second line.
1023        dist_tol (float, optional): Distance tolerance. Defaults to None.
1024
1025    Returns:
1026        bool: True if the lines are close enough, False otherwise.
1027    """
1028    if dist_tol is None:
1029        dist_tol = defaults["dist_tol"]
1030    dist_tol2 = dist_tol * dist_tol
1031    p1, p2 = line1
1032    p3, p4 = line2
1033    return (
1034        close_points2(p1, p3, dist2=dist_tol2)
1035        and close_points2(p2, p4, dist2=dist_tol2)
1036    ) or (
1037        close_points2(p1, p4, dist2=dist_tol2)
1038        and close_points2(p2, p3, dist2=dist_tol2)
1039    )

Return True if two lines are close enough.

Arguments:
  • line1 (Line): First line.
  • line2 (Line): Second line.
  • dist_tol (float, optional): Distance tolerance. Defaults to None.
Returns:

bool: True if the lines are close enough, False otherwise.

def equal_polygons( poly1: Sequence[Sequence[float]], poly2: Sequence[Sequence[float]], dist_tol: float = None) -> bool:
1042def equal_polygons(
1043    poly1: Sequence[Point], poly2: Sequence[Point], dist_tol: float = None
1044) -> bool:
1045    """
1046    Return True if two polygons are close enough.
1047
1048    Args:
1049        poly1 (Sequence[Point]): First polygon.
1050        poly2 (Sequence[Point]): Second polygon.
1051        dist_tol (float, optional): Distance tolerance. Defaults to None.
1052
1053    Returns:
1054        bool: True if the polygons are close enough, False otherwise.
1055    """
1056    if dist_tol is None:
1057        dist_tol = defaults["dist_tol"]
1058    if len(poly1) != len(poly2):
1059        return False
1060    dist_tol2 = dist_tol * dist_tol
1061    for i, pnt in enumerate(poly1):
1062        if not close_points2(pnt, poly2[i], dist2=dist_tol2):
1063            return False
1064    return True

Return True if two polygons are close enough.

Arguments:
  • poly1 (Sequence[Point]): First polygon.
  • poly2 (Sequence[Point]): Second polygon.
  • dist_tol (float, optional): Distance tolerance. Defaults to None.
Returns:

bool: True if the polygons are close enough, False otherwise.

def extended_line( dist: float, line: Sequence[Sequence], extend_both=False) -> Sequence[Sequence]:
1067def extended_line(dist: float, line: Line, extend_both=False) -> Line:
1068    """
1069    Given a line ((x1, y1), (x2, y2)) and a distance,
1070    the given line is extended by distance units.
1071    Return a new line ((x1, y1), (x2', y2')).
1072
1073    Args:
1074        dist (float): Distance to extend the line.
1075        line (Line): Input line.
1076        extend_both (bool, optional): Whether to extend both ends of the line. Defaults to False.
1077
1078    Returns:
1079        Line: Extended line.
1080    """
1081
1082    def extend(dist, line):
1083        # p = (1-t)*p1 + t*p2 : parametric equation of a line segment (p1, p2)
1084        line_length = length(line)
1085        t = (line_length + dist) / line_length
1086        p1, p2 = line
1087        x1, y1 = p1[:2]
1088        x2, y2 = p2[:2]
1089        c = 1 - t
1090
1091        return [(x1, y1), (c * x1 + t * x2, c * y1 + t * y2)]
1092
1093    if extend_both:
1094        p1, p2 = extend(dist, line)
1095        p1, p2 = extend(dist, [p2, p1])
1096        res = [p2, p1]
1097    else:
1098        res = extend(dist, line)
1099
1100    return res

Given a line ((x1, y1), (x2, y2)) and a distance, the given line is extended by distance units. Return a new line ((x1, y1), (x2', y2')).

Arguments:
  • dist (float): Distance to extend the line.
  • line (Line): Input line.
  • extend_both (bool, optional): Whether to extend both ends of the line. Defaults to False.
Returns:

Line: Extended line.

def line_through_point_angle( point: Sequence[float], angle: float, length_: float, both_sides=False) -> Sequence[Sequence]:
1103def line_through_point_angle(
1104    point: Point, angle: float, length_: float, both_sides=False
1105) -> Line:
1106    """
1107    Return a line that passes through the given point
1108    with the given angle and length.
1109    If both_side is True, the line is extended on both sides by the given
1110    length.
1111
1112    Args:
1113        point (Point): Point through which the line passes.
1114        angle (float): Angle of the line in radians.
1115        length_ (float): Length of the line.
1116        both_sides (bool, optional): Whether to extend the line on both sides. Defaults to False.
1117
1118    Returns:
1119        Line: Line passing through the given point with the given angle and length.
1120    """
1121    x, y = point[:2]
1122    line = [(x, y), (x + length_ * cos(angle), y + length_ * sin(angle))]
1123    if both_sides:
1124        p1, p2 = line
1125        line = extended_line(length_, [p2, p1])
1126
1127    return line

Return a line that passes through the given point with the given angle and length. If both_side is True, the line is extended on both sides by the given length.

Arguments:
  • point (Point): Point through which the line passes.
  • angle (float): Angle of the line in radians.
  • length_ (float): Length of the line.
  • both_sides (bool, optional): Whether to extend the line on both sides. Defaults to False.
Returns:

Line: Line passing through the given point with the given angle and length.

def remove_duplicate_points( points: list[typing.Sequence[float]], dist_tol=None) -> list[typing.Sequence[float]]:
1130def remove_duplicate_points(points: list[Point], dist_tol=None) -> list[Point]:
1131    """
1132    Return a list of points with duplicate points removed.
1133
1134    Args:
1135        points (list[Point]): List of points.
1136        dist_tol (float, optional): Distance tolerance. Defaults to None.
1137
1138    Returns:
1139        list[Point]: List of points with duplicate points removed.
1140    """
1141    if dist_tol is None:
1142        dist_tol = defaults["dist_tol"]
1143    new_points = []
1144    for i, point in enumerate(points):
1145        if i == 0:
1146            new_points.append(point)
1147        else:
1148            dist_tol2 = dist_tol * dist_tol
1149            if not close_points2(point, new_points[-1], dist2=dist_tol2):
1150                new_points.append(point)
1151    return new_points

Return a list of points with duplicate points removed.

Arguments:
  • points (list[Point]): List of points.
  • dist_tol (float, optional): Distance tolerance. Defaults to None.
Returns:

list[Point]: List of points with duplicate points removed.

def remove_collinear_points( points: list[typing.Sequence[float]], rtol: float = None, atol: float = None) -> list[typing.Sequence[float]]:
1154def remove_collinear_points(
1155    points: list[Point], rtol: float = None, atol: float = None
1156) -> list[Point]:
1157    """
1158    Return a list of points with collinear points removed.
1159
1160    Args:
1161        points (list[Point]): List of points.
1162        rtol (float, optional): Relative tolerance. Defaults to None.
1163        atol (float, optional): Absolute tolerance. Defaults to None.
1164
1165    Returns:
1166        list[Point]: List of points with collinear points removed.
1167    """
1168    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
1169    new_points = []
1170    for i, point in enumerate(points):
1171        if i == 0:
1172            new_points.append(point)
1173        else:
1174            if not collinear(
1175                new_points[-1], point, points[(i + 1) % len(points)], rtol, atol
1176            ):
1177                new_points.append(point)
1178    return new_points

Return a list of points with collinear points removed.

Arguments:
  • points (list[Point]): List of points.
  • rtol (float, optional): Relative tolerance. Defaults to None.
  • atol (float, optional): Absolute tolerance. Defaults to None.
Returns:

list[Point]: List of points with collinear points removed.

def fix_degen_points( points: list[typing.Sequence[float]], loop=False, closed=False, dist_tol: float = None, area_rtol: float = None, area_atol: float = None, check_collinear=True) -> list[typing.Sequence[float]]:
1181def fix_degen_points(
1182    points: list[Point],
1183    loop=False,
1184    closed=False,
1185    dist_tol: float = None,
1186    area_rtol: float = None,
1187    area_atol: float = None,
1188    check_collinear=True,
1189) -> list[Point]:
1190    """
1191    Return a list of points with duplicate points removed.
1192    Remove the middle point from the collinear points.
1193
1194    Args:
1195        points (list[Point]): List of points.
1196        loop (bool, optional): Whether to loop the points. Defaults to False.
1197        closed (bool, optional): Whether the points form a closed shape. Defaults to False.
1198        dist_tol (float, optional): Distance tolerance. Defaults to None.
1199        area_rtol (float, optional): Relative tolerance for area. Defaults to None.
1200        area_atol (float, optional): Absolute tolerance for area. Defaults to None.
1201        check_collinear (bool, optional): Whether to check for collinear points. Defaults to True.
1202
1203    Returns:
1204        list[Point]: List of points with duplicate and collinear points removed.
1205    """
1206    dist_tol, area_rtol, area_atol = get_defaults(
1207        ["dist_tol", "area_rtol", "area_atol"], [dist_tol, area_rtol, area_atol]
1208    )
1209    dist_tol2 = dist_tol * dist_tol
1210    new_points = []
1211    for i, point in enumerate(points):
1212        if i == 0:
1213            new_points.append(point)
1214        else:
1215            if not close_points2(point, new_points[-1], dist2=dist_tol2):
1216                new_points.append(point)
1217    if loop:
1218        if close_points2(new_points[0], new_points[-1], dist2=dist_tol2):
1219            new_points.pop(-1)
1220
1221    if check_collinear:
1222        # Check for collinear points and remove the middle one.
1223        new_points = merge_consecutive_collinear_edges(
1224            new_points, closed, area_rtol, area_atol
1225        )
1226
1227    return new_points

Return a list of points with duplicate points removed. Remove the middle point from the collinear points.

Arguments:
  • points (list[Point]): List of points.
  • loop (bool, optional): Whether to loop the points. Defaults to False.
  • closed (bool, optional): Whether the points form a closed shape. Defaults to False.
  • dist_tol (float, optional): Distance tolerance. Defaults to None.
  • area_rtol (float, optional): Relative tolerance for area. Defaults to None.
  • area_atol (float, optional): Absolute tolerance for area. Defaults to None.
  • check_collinear (bool, optional): Whether to check for collinear points. Defaults to True.
Returns:

list[Point]: List of points with duplicate and collinear points removed.

def clockwise(p: Sequence[float], q: Sequence[float], r: Sequence[float]) -> bool:
1230def clockwise(p: Point, q: Point, r: Point) -> bool:
1231    """Return 1 if the points p, q, and r are in clockwise order,
1232    return -1 if the points are in counter-clockwise order,
1233    return 0 if the points are collinear
1234
1235    Args:
1236        p (Point): First point.
1237        q (Point): Second point.
1238        r (Point): Third point.
1239
1240    Returns:
1241        int: 1 if the points are in clockwise order, -1 if counter-clockwise, 0 if collinear.
1242    """
1243    area_ = area(p, q, r)
1244    if area_ > 0:
1245        res = 1
1246    elif area_ < 0:
1247        res = -1
1248    else:
1249        res = 0
1250
1251    return res

Return 1 if the points p, q, and r are in clockwise order, return -1 if the points are in counter-clockwise order, return 0 if the points are collinear

Arguments:
  • p (Point): First point.
  • q (Point): Second point.
  • r (Point): Third point.
Returns:

int: 1 if the points are in clockwise order, -1 if counter-clockwise, 0 if collinear.

def intersects(seg1, seg2):
1254def intersects(seg1, seg2):
1255    """Checks if the line segments intersect.
1256    If they are chained together, they are considered as intersecting.
1257    Returns True if the segments intersect, False otherwise.
1258
1259    Args:
1260        seg1 (Line): First line segment.
1261        seg2 (Line): Second line segment.
1262
1263    Returns:
1264        bool: True if the segments intersect, False otherwise.
1265    """
1266    p1, q1 = seg1
1267    p2, q2 = seg2
1268    o1 = clockwise(p1, q1, p2)
1269    o2 = clockwise(p1, q1, q2)
1270    o3 = clockwise(p2, q2, p1)
1271    o4 = clockwise(p2, q2, q1)
1272
1273    if o1 != o2 and o3 != o4:
1274        return True
1275
1276    if o1 == 0 and between(p1, p2, q1):
1277        return True
1278    if o2 == 0 and between(p1, q2, q1):
1279        return True
1280    if o3 == 0 and between(p2, p1, q2):
1281        return True
1282    if o4 == 0 and between(p2, q1, q2):
1283        return True
1284
1285    return False

Checks if the line segments intersect. If they are chained together, they are considered as intersecting. Returns True if the segments intersect, False otherwise.

Arguments:
  • seg1 (Line): First line segment.
  • seg2 (Line): Second line segment.
Returns:

bool: True if the segments intersect, False otherwise.

def is_chained(seg1, seg2):
1288def is_chained(seg1, seg2):
1289    """Checks if the line segments are chained together.
1290
1291    Args:
1292        seg1 (Line): First line segment.
1293        seg2 (Line): Second line segment.
1294
1295    Returns:
1296        bool: True if the segments are chained together, False otherwise.
1297    """
1298    p1, q1 = seg1
1299    p2, q2 = seg2
1300    if (
1301        close_points2(p1, p2)
1302        or close_points2(p1, q2)
1303        or close_points2(q1, p2)
1304        or close_points2(q1, q2)
1305    ):
1306        return True
1307
1308    return False

Checks if the line segments are chained together.

Arguments:
  • seg1 (Line): First line segment.
  • seg2 (Line): Second line segment.
Returns:

bool: True if the segments are chained together, False otherwise.

def direction(p, q, r):
1311def direction(p, q, r):
1312    """
1313    Checks the orientation of three points (p, q, r).
1314
1315    Args:
1316        p (Point): First point.
1317        q (Point): Second point.
1318        r (Point): Third point.
1319
1320    Returns:
1321        int: 0 if collinear, >0 if counter-clockwise, <0 if clockwise.
1322    """
1323    return (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1])

Checks the orientation of three points (p, q, r).

Arguments:
  • p (Point): First point.
  • q (Point): Second point.
  • r (Point): Third point.
Returns:

int: 0 if collinear, >0 if counter-clockwise, <0 if clockwise.

def collinear_segments(segment1, segment2, tol=None, atol=None):
1326def collinear_segments(segment1, segment2, tol=None, atol=None):
1327    """
1328    Checks if two line segments (a1, b1) and (a2, b2) are collinear.
1329
1330    Args:
1331        segment1 (Line): First line segment.
1332        segment2 (Line): Second line segment.
1333        tol (float, optional): Relative tolerance. Defaults to None.
1334        atol (float, optional): Absolute tolerance. Defaults to None.
1335
1336    Returns:
1337        bool: True if the segments are collinear, False otherwise.
1338    """
1339    tol, atol = get_defaults(["tol", "atol"], [tol, atol])
1340    a1, b1 = segment1
1341    a2, b2 = segment2
1342
1343    return isclose(direction(a1, b1, a2), 0, tol, atol) and isclose(
1344        direction(a1, b1, b2), 0, tol, atol
1345    )

Checks if two line segments (a1, b1) and (a2, b2) are collinear.

Arguments:
  • segment1 (Line): First line segment.
  • segment2 (Line): Second line segment.
  • tol (float, optional): Relative tolerance. Defaults to None.
  • atol (float, optional): Absolute tolerance. Defaults to None.
Returns:

bool: True if the segments are collinear, False otherwise.

def global_to_local( x: float, y: float, xi: float, yi: float, theta: float = 0) -> Sequence[float]:
1348def global_to_local(
1349    x: float, y: float, xi: float, yi: float, theta: float = 0
1350) -> Point:
1351    """Given a point(x, y) in global coordinates
1352    and local CS position and orientation,
1353    return a point(ksi, eta) in local coordinates
1354
1355    Args:
1356        x (float): Global x-coordinate.
1357        y (float): Global y-coordinate.
1358        xi (float): Local x-coordinate.
1359        yi (float): Local y-coordinate.
1360        theta (float, optional): Angle in radians. Defaults to 0.
1361
1362    Returns:
1363        Point: Local coordinates (ksi, eta).
1364    """
1365    sin_theta = sin(theta)
1366    cos_theta = cos(theta)
1367    ksi = (x - xi) * cos_theta + (y - yi) * sin_theta
1368    eta = (y - yi) * cos_theta - (x - xi) * sin_theta
1369    return (ksi, eta)

Given a point(x, y) in global coordinates and local CS position and orientation, return a point(ksi, eta) in local coordinates

Arguments:
  • x (float): Global x-coordinate.
  • y (float): Global y-coordinate.
  • xi (float): Local x-coordinate.
  • yi (float): Local y-coordinate.
  • theta (float, optional): Angle in radians. Defaults to 0.
Returns:

Point: Local coordinates (ksi, eta).

def stitch_lines( line1: Sequence[Sequence], line2: Sequence[Sequence]) -> Sequence[Sequence[Sequence]]:
1372def stitch_lines(line1: Line, line2: Line) -> Sequence[Line]:
1373    """if the lines intersect, trim the lines
1374    if the lines don't intersect, extend the lines
1375
1376    Args:
1377        line1 (Line): First line.
1378        line2 (Line): Second line.
1379
1380    Returns:
1381        Sequence[Line]: Trimmed or extended lines.
1382    """
1383    intersection_ = intersect(line1, line2)
1384    res = None
1385    if intersection_:
1386        p1, _ = line1
1387        _, p2 = line2
1388        line1 = [p1, intersection_]
1389        line2 = [intersection_, p2]
1390
1391        res = (line1, line2)
1392
1393    return res

if the lines intersect, trim the lines if the lines don't intersect, extend the lines

Arguments:
  • line1 (Line): First line.
  • line2 (Line): Second line.
Returns:

Sequence[Line]: Trimmed or extended lines.

def get_quadrant(x: float, y: float) -> int:
1396def get_quadrant(x: float, y: float) -> int:
1397    """quadrants:
1398    +x, +y = 1st
1399    +x, -y = 2nd
1400    -x, -y = 3rd
1401    +x, -y = 4th
1402
1403    Args:
1404        x (float): x-coordinate.
1405        y (float): y-coordinate.
1406
1407    Returns:
1408        int: Quadrant number.
1409    """
1410    return int(floor((atan2(y, x) % (TWO_PI)) / (pi / 2)) + 1)

quadrants: +x, +y = 1st +x, -y = 2nd -x, -y = 3rd +x, -y = 4th

Arguments:
  • x (float): x-coordinate.
  • y (float): y-coordinate.
Returns:

int: Quadrant number.

def get_quadrant_from_deg_angle(deg_angle: float) -> int:
1413def get_quadrant_from_deg_angle(deg_angle: float) -> int:
1414    """quadrants:
1415    (0, 90) = 1st
1416    (90, 180) = 2nd
1417    (180, 270) = 3rd
1418    (270, 360) = 4th
1419
1420    Args:
1421        deg_angle (float): Angle in degrees.
1422
1423    Returns:
1424        int: Quadrant number.
1425    """
1426    return int(floor(deg_angle / 90.0) % 4 + 1)

quadrants: (0, 90) = 1st (90, 180) = 2nd (180, 270) = 3rd (270, 360) = 4th

Arguments:
  • deg_angle (float): Angle in degrees.
Returns:

int: Quadrant number.

def homogenize(points: Sequence[Sequence[float]]) -> "'ndarray'":
1429def homogenize(points: Sequence[Point]) -> 'ndarray':
1430    """
1431    Convert a list of points to homogeneous coordinates.
1432
1433    Args:
1434        points (Sequence[Point]): List of points.
1435
1436    Returns:
1437        np.ndarray: Homogeneous coordinates.
1438    """
1439    try:
1440        xy_array = np.array(points, dtype=float)
1441    except ValueError:
1442        xy_array = np.array([p[:2] for p in points], dtype=float)
1443    n_rows, n_cols = xy_array.shape
1444    if n_cols > 2:
1445        xy_array = xy_array[:, :2]
1446    ones = np.ones((n_rows, 1), dtype=float)
1447    homogeneous_array = np.append(xy_array, ones, axis=1)
1448
1449    return homogeneous_array

Convert a list of points to homogeneous coordinates.

Arguments:
  • points (Sequence[Point]): List of points.
Returns:

np.ndarray: Homogeneous coordinates.

def intersect2( x1: float, y1: float, x2: float, y2: float, x3: float, y3: float, x4: float, y4: float, rtol: float = None, atol: float = None) -> Sequence[float]:
1473def intersect2(
1474    x1: float,
1475    y1: float,
1476    x2: float,
1477    y2: float,
1478    x3: float,
1479    y3: float,
1480    x4: float,
1481    y4: float,
1482    rtol: float = None,
1483    atol: float = None,
1484) -> Point:
1485    """Return the intersection point of two lines.
1486    line1: (x1, y1), (x2, y2)
1487    line2: (x3, y3), (x4, y4)
1488    To find the intersection point of two line segments use the
1489    "intersection" function
1490
1491    Args:
1492        x1 (float): x-coordinate of the first point of the first line.
1493        y1 (float): y-coordinate of the first point of the first line.
1494        x2 (float): x-coordinate of the second point of the first line.
1495        y2 (float): y-coordinate of the second point of the first line.
1496        x3 (float): x-coordinate of the first point of the second line.
1497        y3 (float): y-coordinate of the first point of the second line.
1498        x4 (float): x-coordinate of the second point of the second line.
1499        y4 (float): y-coordinate of the second point of the second line.
1500        rtol (float, optional): Relative tolerance. Defaults to None.
1501        atol (float, optional): Absolute tolerance. Defaults to None.
1502
1503    Returns:
1504        Point: Intersection point of the two lines.
1505    """
1506    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
1507    x1_x2 = x1 - x2
1508    y1_y2 = y1 - y2
1509    x3_x4 = x3 - x4
1510    y3_y4 = y3 - y4
1511
1512    denom = (x1_x2) * (y3_y4) - (y1_y2) * (x3_x4)
1513    if isclose(denom, 0, rtol=rtol, atol=atol):
1514        res = None  # parallel lines
1515    else:
1516        x = ((x1 * y2 - y1 * x2) * (x3_x4) - (x1_x2) * (x3 * y4 - y3 * x4)) / denom
1517        y = ((x1 * y2 - y1 * x2) * (y3_y4) - (y1_y2) * (x3 * y4 - y3 * x4)) / denom
1518        res = (x, y)
1519
1520    return res

Return the intersection point of two lines. line1: (x1, y1), (x2, y2) line2: (x3, y3), (x4, y4) To find the intersection point of two line segments use the "intersection" function

Arguments:
  • x1 (float): x-coordinate of the first point of the first line.
  • y1 (float): y-coordinate of the first point of the first line.
  • x2 (float): x-coordinate of the second point of the first line.
  • y2 (float): y-coordinate of the second point of the first line.
  • x3 (float): x-coordinate of the first point of the second line.
  • y3 (float): y-coordinate of the first point of the second line.
  • x4 (float): x-coordinate of the second point of the second line.
  • y4 (float): y-coordinate of the second point of the second line.
  • rtol (float, optional): Relative tolerance. Defaults to None.
  • atol (float, optional): Absolute tolerance. Defaults to None.
Returns:

Point: Intersection point of the two lines.

def intersect(line1: Sequence[Sequence], line2: Sequence[Sequence]) -> Sequence[float]:
1523def intersect(line1: Line, line2: Line) -> Point:
1524    """Return the intersection point of two lines.
1525    line1: [(x1, y1), (x2, y2)]
1526    line2: [(x3, y3), (x4, y4)]
1527    To find the intersection point of two line segments use the
1528    "intersection" function
1529
1530    Args:
1531        line1 (Line): First line.
1532        line2 (Line): Second line.
1533
1534    Returns:
1535        Point: Intersection point of the two lines.
1536    """
1537    x1, y1 = line1[0][:2]
1538    x2, y2 = line1[1][:2]
1539    x3, y3 = line2[0][:2]
1540    x4, y4 = line2[1][:2]
1541    return intersect2(x1, y1, x2, y2, x3, y3, x4, y4)

Return the intersection point of two lines. line1: [(x1, y1), (x2, y2)] line2: [(x3, y3), (x4, y4)] To find the intersection point of two line segments use the "intersection" function

Arguments:
  • line1 (Line): First line.
  • line2 (Line): Second line.
Returns:

Point: Intersection point of the two lines.

def intersection2(x1, y1, x2, y2, x3, y3, x4, y4, rtol=None, atol=None):
1544def intersection2(x1, y1, x2, y2, x3, y3, x4, y4, rtol=None, atol=None):
1545    """Check the intersection of two line segments. See the documentation
1546
1547    Args:
1548        x1 (float): x-coordinate of the first point of the first line segment.
1549        y1 (float): y-coordinate of the first point of the first line segment.
1550        x2 (float): x-coordinate of the second point of the first line segment.
1551        y2 (float): y-coordinate of the second point of the first line segment.
1552        x3 (float): x-coordinate of the first point of the second line segment.
1553        y3 (float): y-coordinate of the first point of the second line segment.
1554        x4 (float): x-coordinate of the second point of the second line segment.
1555        y4 (float): y-coordinate of the second point of the second line segment.
1556        rtol (float, optional): Relative tolerance. Defaults to None.
1557        atol (float, optional): Absolute tolerance. Defaults to None.
1558
1559    Returns:
1560        tuple: Connection type and intersection point.
1561    """
1562    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
1563    x2_x1 = x2 - x1
1564    y2_y1 = y2 - y1
1565    x4_x3 = x4 - x3
1566    y4_y3 = y4 - y3
1567    denom = (y4_y3) * (x2_x1) - (x4_x3) * (y2_y1)
1568    if isclose(denom, 0, rtol=rtol, atol=atol):  # parallel
1569        return Connection.PARALLEL, None
1570    x1_x3 = x1 - x3
1571    y1_y3 = y1 - y3
1572    ua = ((x4_x3) * (y1_y3) - (y4_y3) * (x1_x3)) / denom
1573    if ua < 0 or ua > 1:
1574        return Connection.DISJOINT, None
1575    ub = ((x2_x1) * (y1_y3) - (y2_y1) * (x1_x3)) / denom
1576    if ub < 0 or ub > 1:
1577        return Connection.DISJOINT, None
1578    x = x1 + ua * (x2_x1)
1579    y = y1 + ua * (y2_y1)
1580    return Connection.INTERSECT, (x, y)

Check the intersection of two line segments. See the documentation

Arguments:
  • x1 (float): x-coordinate of the first point of the first line segment.
  • y1 (float): y-coordinate of the first point of the first line segment.
  • x2 (float): x-coordinate of the second point of the first line segment.
  • y2 (float): y-coordinate of the second point of the first line segment.
  • x3 (float): x-coordinate of the first point of the second line segment.
  • y3 (float): y-coordinate of the first point of the second line segment.
  • x4 (float): x-coordinate of the second point of the second line segment.
  • y4 (float): y-coordinate of the second point of the second line segment.
  • rtol (float, optional): Relative tolerance. Defaults to None.
  • atol (float, optional): Absolute tolerance. Defaults to None.
Returns:

tuple: Connection type and intersection point.

def intersection3( x1: float, y1: float, x2: float, y2: float, x3: float, y3: float, x4: float, y4: float, rtol: float = None, atol: float = None, dist_tol: float = None, area_atol: float = None) -> tuple[simetri.graphics.all_enums.Connection, list]:
1583def intersection3(
1584    x1: float,
1585    y1: float,
1586    x2: float,
1587    y2: float,
1588    x3: float,
1589    y3: float,
1590    x4: float,
1591    y4: float,
1592    rtol: float = None,
1593    atol: float = None,
1594    dist_tol: float = None,
1595    area_atol: float = None,
1596) -> tuple[Connection, list]:
1597    """Check the intersection of two line segments. See the documentation
1598    for more details.
1599
1600    Args:
1601        x1 (float): x-coordinate of the first point of the first line segment.
1602        y1 (float): y-coordinate of the first point of the first line segment.
1603        x2 (float): x-coordinate of the second point of the first line segment.
1604        y2 (float): y-coordinate of the second point of the first line segment.
1605        x3 (float): x-coordinate of the first point of the second line segment.
1606        y3 (float): y-coordinate of the first point of the second line segment.
1607        x4 (float): x-coordinate of the second point of the second line segment.
1608        y4 (float): y-coordinate of the second point of the second line segment.
1609        rtol (float, optional): Relative tolerance. Defaults to None.
1610        atol (float, optional): Absolute tolerance. Defaults to None.
1611        dist_tol (float, optional): Distance tolerance. Defaults to None.
1612        area_atol (float, optional): Absolute tolerance for area. Defaults to None.
1613
1614    Returns:
1615        tuple: Connection type and intersection result.
1616    """
1617    # collinear check uses area_atol
1618
1619    # s1: start1 = (x1, y1)
1620    # e1: end1 = (x2, y2)
1621    # s2: start2 = (x3, y3)
1622    # e2: end2 = (x4, y4)
1623    # s1s2: start1 and start2 is connected
1624    # s1e2: start1 and end2 is connected
1625    # e1s2: end1 and start2 is connected
1626    # e1e2: end1 and end2 is connected
1627    rtol, atol, dist_tol, area_atol = get_defaults(
1628        ["rtol", "atol", "dist_tol", "area_atol"], [rtol, atol, dist_tol, area_atol]
1629    )
1630
1631    s1 = (x1, y1)
1632    e1 = (x2, y2)
1633    s2 = (x3, y3)
1634    e2 = (x4, y4)
1635    segment1 = [(x1, y1), (x2, y2)]
1636    segment2 = [(x3, y3), (x4, y4)]
1637
1638    # check if the segments' bounding boxes overlap
1639    if not line_segment_bbox_check(segment1, segment2):
1640        return (Connection.DISJOINT, None)
1641
1642    # Check if the segments are parallel
1643    x2_x1 = x2 - x1
1644    y2_y1 = y2 - y1
1645    x4_x3 = x4 - x3
1646    y4_y3 = y4 - y3
1647    denom = (y4_y3) * (x2_x1) - (x4_x3) * (y2_y1)
1648    parallel = isclose(denom, 0, rtol=rtol, atol=atol)
1649    # angle1 = atan2(y2 - y1, x2 - x1) % pi
1650    # angle2 = atan2(y4 - y3, x4 - x3) % pi
1651    # parallel = close_angles(angle1, angle2, angtol=defaults['angtol'])
1652
1653    # Coincident end points
1654    dist_tol2 = dist_tol * dist_tol
1655    s1s2 = close_points2(s1, s2, dist2=dist_tol2)
1656    s1e2 = close_points2(s1, e2, dist2=dist_tol2)
1657    e1s2 = close_points2(e1, s2, dist2=dist_tol2)
1658    e1e2 = close_points2(e1, e2, dist2=dist_tol2)
1659    connected = s1s2 or s1e2 or e1s2 or e1e2
1660    if parallel:
1661        length1 = distance((x1, y1), (x2, y2))
1662        length2 = distance((x3, y3), (x4, y4))
1663        min_x = min(x1, x2, x3, x4)
1664        max_x = max(x1, x2, x3, x4)
1665        min_y = min(y1, y2, y3, y4)
1666        max_y = max(y1, y2, y3, y4)
1667        total_length = distance((min_x, min_y), (max_x, max_y))
1668        l1_eq_l2 = isclose(length1, length2, rtol=rtol, atol=atol)
1669        l1_eq_total = isclose(length1, total_length, rtol=rtol, atol=atol)
1670        l2_eq_total = isclose(length2, total_length, rtol=rtol, atol=atol)
1671        if connected:
1672            if l1_eq_l2 and l1_eq_total:
1673                return Connection.CONGRUENT, segment1
1674
1675            if l1_eq_total:
1676                return Connection.CONTAINS, segment1
1677            if l2_eq_total:
1678                return Connection.WITHIN, segment2
1679            if isclose(length1 + length2, total_length, rtol, atol):
1680                # chained and collienar
1681                if s1s2:
1682                    return Connection.COLL_CHAIN, (e1, s1, e2)
1683                if s1e2:
1684                    return Connection.COLL_CHAIN, (e1, s1, s2)
1685                if e1s2:
1686                    return Connection.COLL_CHAIN, (s1, s2, e2)
1687                if e1e2:
1688                    return Connection.COLL_CHAIN, (s1, e1, s2)
1689        else:
1690            if total_length < length1 + length2 and collinear_segments(
1691                segment1, segment2, atol
1692            ):
1693                p1 = (min_x, min_y)
1694                p2 = (max_x, max_y)
1695                seg = [p1, p2]
1696                return Connection.OVERLAPS, seg
1697
1698            return intersection2(x1, y1, x2, y2, x3, y3, x4, y4, rtol, atol)
1699    else:
1700        if connected:
1701            if s1s2:
1702                return Connection.CHAIN, (e1, s1, e2)
1703            if s1e2:
1704                return Connection.CHAIN, (e1, s1, s2)
1705            if e1s2:
1706                return Connection.CHAIN, (s1, s2, e2)
1707            if e1e2:
1708                return Connection.CHAIN, (s1, e1, s2)
1709        else:
1710            if between(s1, e1, e2):
1711                return Connection.YJOINT, e1
1712            if between(s1, e1, s2):
1713                return Connection.YJOINT, s1
1714            if between(s2, e2, e1):
1715                return Connection.YJOINT, e2
1716            if between(s2, e2, s1):
1717                return Connection.YJOINT, s2
1718
1719            return intersection2(x1, y1, x2, y2, x3, y3, x4, y4, rtol, atol)
1720    return (Connection.DISJOINT, None)

Check the intersection of two line segments. See the documentation for more details.

Arguments:
  • x1 (float): x-coordinate of the first point of the first line segment.
  • y1 (float): y-coordinate of the first point of the first line segment.
  • x2 (float): x-coordinate of the second point of the first line segment.
  • y2 (float): y-coordinate of the second point of the first line segment.
  • x3 (float): x-coordinate of the first point of the second line segment.
  • y3 (float): y-coordinate of the first point of the second line segment.
  • x4 (float): x-coordinate of the second point of the second line segment.
  • y4 (float): y-coordinate of the second point of the second line segment.
  • rtol (float, optional): Relative tolerance. Defaults to None.
  • atol (float, optional): Absolute tolerance. Defaults to None.
  • dist_tol (float, optional): Distance tolerance. Defaults to None.
  • area_atol (float, optional): Absolute tolerance for area. Defaults to None.
Returns:

tuple: Connection type and intersection result.

def merge_consecutive_collinear_edges(points, closed=False, area_rtol=None, area_atol=None):
1723def merge_consecutive_collinear_edges(
1724    points, closed=False, area_rtol=None, area_atol=None
1725):
1726    """Remove the middle points from collinear edges.
1727
1728    Args:
1729        points (list[Point]): List of points.
1730        closed (bool, optional): Whether the points form a closed shape. Defaults to False.
1731        area_rtol (float, optional): Relative tolerance for area. Defaults to None.
1732        area_atol (float, optional): Absolute tolerance for area. Defaults to None.
1733
1734    Returns:
1735        list[Point]: List of points with collinear points removed.
1736    """
1737    area_rtol, area_atol = get_defaults(
1738        ["area_rtol", "area_atol"], [area_rtol, area_atol]
1739    )
1740    points = points[:]
1741
1742    while True:
1743        cyc = cycle(points)
1744        a = next(cyc)
1745        b = next(cyc)
1746        c = next(cyc)
1747        looping = False
1748        n = len(points) - 1
1749        if closed:
1750            n += 1
1751        discarded = []
1752        for _ in range(n - 1):
1753            if collinear(a, b, c, area_rtol=area_rtol, area_atol=area_atol):
1754                discarded.append(b)
1755                looping = True
1756                break
1757            a = b
1758            b = c
1759            c = next(cyc)
1760        for point in discarded:
1761            points.remove(point)
1762        if not looping or len(points) < 3:
1763            break
1764
1765    return points

Remove the middle points from collinear edges.

Arguments:
  • points (list[Point]): List of points.
  • closed (bool, optional): Whether the points form a closed shape. Defaults to False.
  • area_rtol (float, optional): Relative tolerance for area. Defaults to None.
  • area_atol (float, optional): Absolute tolerance for area. Defaults to None.
Returns:

list[Point]: List of points with collinear points removed.

def intersection( line1: Sequence[Sequence], line2: Sequence[Sequence], rtol: float = None) -> int:
1768def intersection(line1: Line, line2: Line, rtol: float = None) -> int:
1769    """return the intersection point of two line segments.
1770    segment1: ((x1, y1), (x2, y2))
1771    segment2: ((x3, y3), (x4, y4))
1772    if line segments do not intersect return -1
1773    if line segments are parallel return 0
1774    if line segments are connected (share a point) return 1
1775    To find the intersection point of two lines use the "intersect" function
1776
1777    Args:
1778        line1 (Line): First line segment.
1779        line2 (Line): Second line segment.
1780        rtol (float, optional): Relative tolerance. Defaults to None.
1781
1782    Returns:
1783        int: Intersection type.
1784    """
1785    if rtol is None:
1786        rtol = defaults["rtol"]
1787    x1, y1 = line1[0]
1788    x2, y2 = line1[1]
1789    x3, y3 = line2[0]
1790    x4, y4 = line2[1]
1791    return intersection2(x1, y1, x2, y2, x3, y3, x4, y4)

return the intersection point of two line segments. segment1: ((x1, y1), (x2, y2)) segment2: ((x3, y3), (x4, y4)) if line segments do not intersect return -1 if line segments are parallel return 0 if line segments are connected (share a point) return 1 To find the intersection point of two lines use the "intersect" function

Arguments:
  • line1 (Line): First line segment.
  • line2 (Line): Second line segment.
  • rtol (float, optional): Relative tolerance. Defaults to None.
Returns:

int: Intersection type.

def merge_segments( seg1: Sequence[Sequence[float]], seg2: Sequence[Sequence[float]]) -> Sequence[Sequence[float]]:
1794def merge_segments(seg1: Sequence[Point], seg2: Sequence[Point]) -> Sequence[Point]:
1795    """Merge two segments into one segment if they are connected.
1796    They need to be overlapping or simply connected to each other,
1797    otherwise they will not be merged. Order doesn't matter.
1798
1799    Args:
1800        seg1 (Sequence[Point]): First segment.
1801        seg2 (Sequence[Point]): Second segment.
1802
1803    Returns:
1804        Sequence[Point]: Merged segment.
1805    """
1806    Conn = Connection
1807    p1, p2 = seg1
1808    p3, p4 = seg2
1809
1810    res = all_intersections([(p1, p2), (p3, p4)], use_intersection3=True)
1811    if res:
1812        conn_type = list(res.values())[0][0][0]
1813        verts = list(res.values())[0][0][1]
1814        if conn_type in [Conn.OVERLAPS, Conn.CONGRUENT, Conn.CHAIN]:
1815            res = verts
1816        elif conn_type == Conn.COLL_CHAIN:
1817            res = (verts[0], verts[1])
1818        else:
1819            res = None
1820    else:
1821        res = None  # need this to avoid returning an empty dict
1822
1823    return res

Merge two segments into one segment if they are connected. They need to be overlapping or simply connected to each other, otherwise they will not be merged. Order doesn't matter.

Arguments:
  • seg1 (Sequence[Point]): First segment.
  • seg2 (Sequence[Point]): Second segment.
Returns:

Sequence[Point]: Merged segment.

def invert(p, center, radius):
1826def invert(p, center, radius):
1827    """Inverts p about a circle at the given center and radius
1828
1829    Args:
1830        p (Point): Point to invert.
1831        center (Point): Center of the circle.
1832        radius (float): Radius of the circle.
1833
1834    Returns:
1835        Point: Inverted point.
1836    """
1837    dist = distance(p, center)
1838    if dist == 0:
1839        return p
1840    p = np.array(p)
1841    center = np.array(center)
1842    return center + (radius**2 / dist**2) * (p - center)
1843    # return radius**2 * (p - center) / dist

Inverts p about a circle at the given center and radius

Arguments:
  • p (Point): Point to invert.
  • center (Point): Center of the circle.
  • radius (float): Radius of the circle.
Returns:

Point: Inverted point.

def is_horizontal(line: Sequence[Sequence], eps: float = 0.0001) -> bool:
1846def is_horizontal(line: Line, eps: float = 0.0001) -> bool:
1847    """Return True if the line is horizontal.
1848
1849    Args:
1850        line (Line): Input line.
1851        eps (float, optional): Tolerance. Defaults to 0.0001.
1852
1853    Returns:
1854        bool: True if the line is horizontal, False otherwise.
1855    """
1856    return abs(j_vec.dot(line_vector(line))) <= eps

Return True if the line is horizontal.

Arguments:
  • line (Line): Input line.
  • eps (float, optional): Tolerance. Defaults to 0.0001.
Returns:

bool: True if the line is horizontal, False otherwise.

def is_line(line_: Any) -> bool:
1859def is_line(line_: Any) -> bool:
1860    """Return True if the input is a line.
1861
1862    Args:
1863        line_ (Any): Input value.
1864
1865    Returns:
1866        bool: True if the input is a line, False otherwise.
1867    """
1868    try:
1869        p1, p2 = line_
1870        return is_point(p1) and is_point(p2)
1871    except:
1872        return False

Return True if the input is a line.

Arguments:
  • line_ (Any): Input value.
Returns:

bool: True if the input is a line, False otherwise.

def is_point(pnt: Any) -> bool:
1875def is_point(pnt: Any) -> bool:
1876    """Return True if the input is a point.
1877
1878    Args:
1879        pnt (Any): Input value.
1880
1881    Returns:
1882        bool: True if the input is a point, False otherwise.
1883    """
1884    try:
1885        x, y = pnt[:2]
1886        return is_number(x) and is_number(y)
1887    except:
1888        return False

Return True if the input is a point.

Arguments:
  • pnt (Any): Input value.
Returns:

bool: True if the input is a point, False otherwise.

def is_vertical(line: Sequence[Sequence], eps: float = 0.0001) -> bool:
1891def is_vertical(line: Line, eps: float = 0.0001) -> bool:
1892    """Return True if the line is vertical.
1893
1894    Args:
1895        line (Line): Input line.
1896        eps (float, optional): Tolerance. Defaults to 0.0001.
1897
1898    Returns:
1899        bool: True if the line is vertical, False otherwise.
1900    """
1901    return abs(i_vec.dot(line_vector(line))) <= eps

Return True if the line is vertical.

Arguments:
  • line (Line): Input line.
  • eps (float, optional): Tolerance. Defaults to 0.0001.
Returns:

bool: True if the line is vertical, False otherwise.

def length(line: Sequence[Sequence]) -> float:
1904def length(line: Line) -> float:
1905    """Return the length of a line.
1906
1907    Args:
1908        line (Line): Input line.
1909
1910    Returns:
1911        float: Length of the line.
1912    """
1913    p1, p2 = line
1914    return distance(p1, p2)

Return the length of a line.

Arguments:
  • line (Line): Input line.
Returns:

float: Length of the line.

def lerp_point(p1: Sequence[float], p2: Sequence[float], t: float) -> Sequence[float]:
1917def lerp_point(p1: Point, p2: Point, t: float) -> Point:
1918    """Linear interpolation of two points.
1919
1920    Args:
1921        p1 (Point): First point.
1922        p2 (Point): Second point.
1923        t (float): Interpolation parameter. t = 0 => p1, t = 1 => p2.
1924
1925    Returns:
1926        Point: Interpolated point.
1927    """
1928    x1, y1 = p1
1929    x2, y2 = p2
1930    return (lerp(x1, x2, t), lerp(y1, y2, t))

Linear interpolation of two points.

Arguments:
  • p1 (Point): First point.
  • p2 (Point): Second point.
  • t (float): Interpolation parameter. t = 0 => p1, t = 1 => p2.
Returns:

Point: Interpolated point.

def slope( start_point: Sequence[float], end_point: Sequence[float], rtol=None, atol=None) -> float:
1933def slope(start_point: Point, end_point: Point, rtol=None, atol=None) -> float:
1934    """Return the slope of a line given by two points.
1935    Order makes a difference.
1936
1937    Args:
1938        start_point (Point): Start point of the line.
1939        end_point (Point): End point of the line.
1940        rtol (float, optional): Relative tolerance. Defaults to None.
1941        atol (float, optional): Absolute tolerance. Defaults to None.
1942
1943    Returns:
1944        float: Slope of the line.
1945    """
1946    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
1947    x1, y1 = start_point[:2]
1948    x2, y2 = end_point[:2]
1949    if isclose(x1, x2, rtol=rtol, atol=atol):
1950        res = defaults["INF"]
1951    else:
1952        res = (y2 - y1) / (x2 - x1)
1953
1954    return res

Return the slope of a line given by two points. Order makes a difference.

Arguments:
  • start_point (Point): Start point of the line.
  • end_point (Point): End point of the line.
  • rtol (float, optional): Relative tolerance. Defaults to None.
  • atol (float, optional): Absolute tolerance. Defaults to None.
Returns:

float: Slope of the line.

def segmentize_line( line: Sequence[Sequence], segment_length: float) -> list[typing.Sequence[typing.Sequence]]:
1957def segmentize_line(line: Line, segment_length: float) -> list[Line]:
1958    """Return a list of points that would form segments with the given length.
1959
1960    Args:
1961        line (Line): Input line.
1962        segment_length (float): Length of each segment.
1963
1964    Returns:
1965        list[Line]: List of segments.
1966    """
1967    length_ = distance(line[0], line[1])
1968    x1, y1 = line[0]
1969    x2, y2 = line[1]
1970    increments = int(length_ / segment_length)
1971    x_segments = np.linspace(x1, x2, increments)
1972    y_segments = np.linspace(y1, y2, increments)
1973
1974    return list(zip(x_segments, y_segments))

Return a list of points that would form segments with the given length.

Arguments:
  • line (Line): Input line.
  • segment_length (float): Length of each segment.
Returns:

list[Line]: List of segments.

def line_angle(start_point: Sequence[float], end_point: Sequence[float]) -> float:
1977def line_angle(start_point: Point, end_point: Point) -> float:
1978    """Return the orientation angle (in radians) of a line given by start and end points.
1979    Order makes a difference.
1980
1981    Args:
1982        start_point (Point): Start point of the line.
1983        end_point (Point): End point of the line.
1984
1985    Returns:
1986        float: Orientation angle of the line in radians.
1987    """
1988    return atan2(end_point[1] - start_point[1], end_point[0] - start_point[0])

Return the orientation angle (in radians) of a line given by start and end points. Order makes a difference.

Arguments:
  • start_point (Point): Start point of the line.
  • end_point (Point): End point of the line.
Returns:

float: Orientation angle of the line in radians.

def inclination_angle(start_point: Sequence[float], end_point: Sequence[float]) -> float:
1991def inclination_angle(start_point: Point, end_point: Point) -> float:
1992    """Return the inclination angle (in radians) of a line given by start and end points.
1993    Inclination angle is always between zero and pi.
1994    Order makes no difference.
1995
1996    Args:
1997        start_point (Point): Start point of the line.
1998        end_point (Point): End point of the line.
1999
2000    Returns:
2001        float: Inclination angle of the line in radians.
2002    """
2003    return line_angle(start_point, end_point) % pi

Return the inclination angle (in radians) of a line given by start and end points. Inclination angle is always between zero and pi. Order makes no difference.

Arguments:
  • start_point (Point): Start point of the line.
  • end_point (Point): End point of the line.
Returns:

float: Inclination angle of the line in radians.

def line2vector(line: Sequence[Sequence]) -> Sequence[float]:
2006def line2vector(line: Line) -> VecType:
2007    """Return the vector representation of a line
2008
2009    Args:
2010        line (Line): Input line.
2011
2012    Returns:
2013        VecType: Vector representation of the line.
2014    """
2015    x1, y1 = line[0]
2016    x2, y2 = line[1]
2017    dx = x2 - x1
2018    dy = y2 - y1
2019    return [dx, dy]

Return the vector representation of a line

Arguments:
  • line (Line): Input line.
Returns:

VecType: Vector representation of the line.

def line_through_point_and_angle( point: Sequence[float], angle: float, length_: float = 100) -> Sequence[Sequence]:
2022def line_through_point_and_angle(
2023    point: Point, angle: float, length_: float = 100
2024) -> Line:
2025    """Return a line through the given point with the given angle and length
2026
2027    Args:
2028        point (Point): Point through which the line passes.
2029        angle (float): Angle of the line in radians.
2030        length_ (float, optional): Length of the line. Defaults to 100.
2031
2032    Returns:
2033        Line: Line passing through the given point with the given angle and length.
2034    """
2035    x, y = point[:2]
2036    dx = length_ * cos(angle)
2037    dy = length_ * sin(angle)
2038    return [[x, y], [x + dx, y + dy]]

Return a line through the given point with the given angle and length

Arguments:
  • point (Point): Point through which the line passes.
  • angle (float): Angle of the line in radians.
  • length_ (float, optional): Length of the line. Defaults to 100.
Returns:

Line: Line passing through the given point with the given angle and length.

def line_vector(line: Sequence[Sequence]) -> Sequence[float]:
2041def line_vector(line: Line) -> VecType:
2042    """Return the vector representation of a line.
2043
2044    Args:
2045        line (Line): Input line.
2046
2047    Returns:
2048        VecType: Vector representation of the line.
2049    """
2050    x1, y1 = line[0]
2051    x2, y2 = line[1]
2052    return Vector2D(x2 - x1, y2 - y1)

Return the vector representation of a line.

Arguments:
  • line (Line): Input line.
Returns:

VecType: Vector representation of the line.

def mid_point(p1: Sequence[float], p2: Sequence[float]) -> Sequence[float]:
2055def mid_point(p1: Point, p2: Point) -> Point:
2056    """Return the mid point of two points.
2057
2058    Args:
2059        p1 (Point): First point.
2060        p2 (Point): Second point.
2061
2062    Returns:
2063        Point: Mid point of the two points.
2064    """
2065    x = (p2[0] + p1[0]) / 2
2066    y = (p2[1] + p1[1]) / 2
2067    return (x, y)

Return the mid point of two points.

Arguments:
  • p1 (Point): First point.
  • p2 (Point): Second point.
Returns:

Point: Mid point of the two points.

def norm(vec: Sequence[float]) -> float:
2070def norm(vec: VecType) -> float:
2071    """Return the norm (vector length) of a vector.
2072
2073    Args:
2074        vec (VecType): Input vector.
2075
2076    Returns:
2077        float: Norm of the vector.
2078    """
2079    return hypot(vec[0], vec[1])

Return the norm (vector length) of a vector.

Arguments:
  • vec (VecType): Input vector.
Returns:

float: Norm of the vector.

def ndarray_to_xy_list(arr: "'ndarray'") -> Sequence[Sequence[float]]:
2082def ndarray_to_xy_list(arr: 'ndarray') -> Sequence[Point]:
2083    """Convert a numpy array to a list of points.
2084
2085    Args:
2086        arr (np.ndarray): Input numpy array.
2087
2088    Returns:
2089        Sequence[Point]: List of points.
2090    """
2091    return arr[:, :2].tolist()

Convert a numpy array to a list of points.

Arguments:
  • arr (np.ndarray): Input numpy array.
Returns:

Sequence[Point]: List of points.

def offset_line( line: Sequence[Sequence[float]], offset: float) -> Sequence[Sequence[float]]:
2094def offset_line(line: Sequence[Point], offset: float) -> Sequence[Point]:
2095    """Return an offset line from a given line.
2096
2097    Args:
2098        line (Sequence[Point]): Input line.
2099        offset (float): Offset distance.
2100
2101    Returns:
2102        Sequence[Point]: Offset line.
2103    """
2104    unit_vec = perp_unit_vector(line)
2105    dx = unit_vec[0] * offset
2106    dy = unit_vec[1] * offset
2107    x1, y1 = line[0]
2108    x2, y2 = line[1]
2109    return [[x1 + dx, y1 + dy], [x2 + dx, y2 + dy]]

Return an offset line from a given line.

Arguments:
  • line (Sequence[Point]): Input line.
  • offset (float): Offset distance.
Returns:

Sequence[Point]: Offset line.

def offset_lines( polylines: Sequence[Sequence[Sequence]], offset: float = 1) -> list[typing.Sequence[typing.Sequence]]:
2112def offset_lines(polylines: Sequence[Line], offset: float = 1) -> list[Line]:
2113    """Return a list of offset lines from a list of lines.
2114
2115    Args:
2116        polylines (Sequence[Line]): List of input lines.
2117        offset (float, optional): Offset distance. Defaults to 1.
2118
2119    Returns:
2120        list[Line]: List of offset lines.
2121    """
2122
2123    def stitch_(polyline):
2124        res = []
2125        line1 = polyline[0]
2126        for i, _ in enumerate(polyline):
2127            if i == len(polyline) - 1:
2128                break
2129            line2 = polyline[i + 1]
2130            line1, line2 = stitch_lines(line1, line2)
2131            res.extend(line1)
2132            line1 = line2
2133        res.append(line2[-1])
2134        return res
2135
2136    poly = []
2137    for line in polylines:
2138        poly.append(offset_line(line, offset))
2139    poly = stitch_(poly)
2140    return poly

Return a list of offset lines from a list of lines.

Arguments:
  • polylines (Sequence[Line]): List of input lines.
  • offset (float, optional): Offset distance. Defaults to 1.
Returns:

list[Line]: List of offset lines.

def normalize(vec: Sequence[float]) -> Sequence[float]:
2143def normalize(vec: VecType) -> VecType:
2144    """Return the normalized vector.
2145
2146    Args:
2147        vec (VecType): Input vector.
2148
2149    Returns:
2150        VecType: Normalized vector.
2151    """
2152    norm_ = norm(vec)
2153    return [vec[0] / norm_, vec[1] / norm_]

Return the normalized vector.

Arguments:
  • vec (VecType): Input vector.
Returns:

VecType: Normalized vector.

def offset_point_on_line( point: Sequence[float], line: Sequence[Sequence], offset: float) -> Sequence[float]:
2156def offset_point_on_line(point: Point, line: Line, offset: float) -> Point:
2157    """Return a point on a line that is offset from the given point.
2158
2159    Args:
2160        point (Point): Input point.
2161        line (Line): Input line.
2162        offset (float): Offset distance.
2163
2164    Returns:
2165        Point: Offset point on the line.
2166    """
2167    x, y = point[:2]
2168    x1, y1 = line[0]
2169    x2, y2 = line[1]
2170    dx = x2 - x1
2171    dy = y2 - y1
2172    # normalize the vector
2173    mag = (dx * dx + dy * dy) ** 0.5
2174    dx = dx / mag
2175    dy = dy / mag
2176    return x + dx * offset, y + dy * offset

Return a point on a line that is offset from the given point.

Arguments:
  • point (Point): Input point.
  • line (Line): Input line.
  • offset (float): Offset distance.
Returns:

Point: Offset point on the line.

def offset_point(point: Sequence[float], dx: float = 0, dy: float = 0) -> Sequence[float]:
2179def offset_point(point: Point, dx: float = 0, dy: float = 0) -> Point:
2180    """Return an offset point from a given point.
2181
2182    Args:
2183        point (Point): Input point.
2184        dx (float, optional): Offset distance in x-direction. Defaults to 0.
2185        dy (float, optional): Offset distance in y-direction. Defaults to 0.
2186
2187    Returns:
2188        Point: Offset point.
2189    """
2190    x, y = point[:2]
2191    return x + dx, y + dy

Return an offset point from a given point.

Arguments:
  • point (Point): Input point.
  • dx (float, optional): Offset distance in x-direction. Defaults to 0.
  • dy (float, optional): Offset distance in y-direction. Defaults to 0.
Returns:

Point: Offset point.

def parallel_line(line: Sequence[Sequence], point: Sequence[float]) -> Sequence[Sequence]:
2194def parallel_line(line: Line, point: Point) -> Line:
2195    """Return a parallel line to the given line that goes through the given point
2196
2197    Args:
2198        line (Line): Input line.
2199        point (Point): Point through which the parallel line passes.
2200
2201    Returns:
2202        Line: Parallel line.
2203    """
2204    x1, y1 = line[0]
2205    x2, y2 = line[1]
2206    x3, y3 = point
2207    dx = x2 - x1
2208    dy = y2 - y1
2209    return [[x3, y3], [x3 + dx, y3 + dy]]

Return a parallel line to the given line that goes through the given point

Arguments:
  • line (Line): Input line.
  • point (Point): Point through which the parallel line passes.
Returns:

Line: Parallel line.

def perp_offset_point( point: Sequence[float], line: Sequence[Sequence], offset: float) -> Sequence[float]:
2212def perp_offset_point(point: Point, line: Line, offset: float) -> Point:
2213    """Return a point that is offset from the given point in the perpendicular direction to the given line.
2214
2215    Args:
2216        point (Point): Input point.
2217        line (Line): Input line.
2218        offset (float): Offset distance.
2219
2220    Returns:
2221        Point: Perpendicular offset point.
2222    """
2223    unit_vec = perp_unit_vector(line)
2224    dx = unit_vec[0] * offset
2225    dy = unit_vec[1] * offset
2226    x, y = point[:2]
2227    return [x + dx, y + dy]

Return a point that is offset from the given point in the perpendicular direction to the given line.

Arguments:
  • point (Point): Input point.
  • line (Line): Input line.
  • offset (float): Offset distance.
Returns:

Point: Perpendicular offset point.

def perp_unit_vector(line: Sequence[Sequence]) -> Sequence[float]:
2230def perp_unit_vector(line: Line) -> VecType:
2231    """Return the perpendicular unit vector to a line
2232
2233    Args:
2234        line (Line): Input line.
2235
2236    Returns:
2237        VecType: Perpendicular unit vector.
2238    """
2239    x1, y1 = line[0]
2240    x2, y2 = line[1]
2241    dx = x2 - x1
2242    dy = y2 - y1
2243    norm_ = sqrt(dx**2 + dy**2)
2244    return [-dy / norm_, dx / norm_]

Return the perpendicular unit vector to a line

Arguments:
  • line (Line): Input line.
Returns:

VecType: Perpendicular unit vector.

def point_on_line( point: Sequence[float], line: Sequence[Sequence], rtol: float = None, atol: float = None) -> bool:
2247def point_on_line(
2248    point: Point, line: Line, rtol: float = None, atol: float = None
2249) -> bool:
2250    """Return True if the given point is on the given line
2251
2252    Args:
2253        point (Point): Input point.
2254        line (Line): Input line.
2255        rtol (float, optional): Relative tolerance. Defaults to None.
2256        atol (float, optional): Absolute tolerance. Defaults to None.
2257
2258    Returns:
2259        bool: True if the point is on the line, False otherwise.
2260    """
2261    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
2262    p1, p2 = line
2263    return isclose(slope(p1, point), slope(point, p2), rtol=rtol, atol=atol)

Return True if the given point is on the given line

Arguments:
  • point (Point): Input point.
  • line (Line): Input line.
  • rtol (float, optional): Relative tolerance. Defaults to None.
  • atol (float, optional): Absolute tolerance. Defaults to None.
Returns:

bool: True if the point is on the line, False otherwise.

def point_on_line_segment( point: Sequence[float], line: Sequence[Sequence], rtol: float = None, atol: float = None) -> bool:
2266def point_on_line_segment(
2267    point: Point, line: Line, rtol: float = None, atol: float = None
2268) -> bool:
2269    """Return True if the given point is on the given line segment
2270
2271    Args:
2272        point (Point): Input point.
2273        line (Line): Input line segment.
2274        rtol (float, optional): Relative tolerance. Defaults to None.
2275        atol (float, optional): Absolute tolerance. Defaults to None.
2276
2277    Returns:
2278        bool: True if the point is on the line segment, False otherwise.
2279    """
2280    rtol, atol = get_defaults(["rtol", "atol"], [rtol, atol])
2281    p1, p2 = line
2282    return isclose(
2283        (distance(p1, point) + distance(p2, point)),
2284        distance(p1, p2),
2285        rtol=rtol,
2286        atol=atol,
2287    )

Return True if the given point is on the given line segment

Arguments:
  • point (Point): Input point.
  • line (Line): Input line segment.
  • rtol (float, optional): Relative tolerance. Defaults to None.
  • atol (float, optional): Absolute tolerance. Defaults to None.
Returns:

bool: True if the point is on the line segment, False otherwise.

def point_to_line_distance(point: Sequence[float], line: Sequence[Sequence]) -> float:
2290def point_to_line_distance(point: Point, line: Line) -> float:
2291    """Return the vector from a point to a line
2292
2293    Args:
2294        point (Point): Input point.
2295        line (Line): Input line.
2296
2297    Returns:
2298        float: Distance from the point to the line.
2299    """
2300    x0, y0 = point
2301    x1, y1 = line[0]
2302    x2, y2 = line[1]
2303    dx = x2 - x1
2304    dy = y2 - y1
2305    return abs((dx * (y1 - y0) - (x1 - x0) * dy)) / sqrt(dx**2 + dy**2)

Return the vector from a point to a line

Arguments:
  • point (Point): Input point.
  • line (Line): Input line.
Returns:

float: Distance from the point to the line.

def point_to_line_seg_distance(p, lp1, lp2):
2308def point_to_line_seg_distance(p, lp1, lp2):
2309    """Given a point p and a line segment defined by boundary points
2310    lp1 and lp2, returns the distance between the line segment and the point.
2311    If the point is not located in the perpendicular area between the
2312    boundary points, returns False.
2313
2314    Args:
2315        p (Point): Input point.
2316        lp1 (Point): First boundary point of the line segment.
2317        lp2 (Point): Second boundary point of the line segment.
2318
2319    Returns:
2320        float: Distance between the point and the line segment, or False if the point is not in the perpendicular area.
2321    """
2322    if lp1[:2] == lp2[:2]:
2323        msg = "Error! Line is ill defined. Start and end points are coincident."
2324        raise ValueError(msg)
2325    x3, y3 = p[:2]
2326    x1, y1 = lp1[:2]
2327    x2, y2 = lp2[:2]
2328
2329    u = ((x3 - x1) * (x2 - x1) + (y3 - y1) * (y2 - y1)) / distance(lp1, lp2) ** 2
2330    if 0 <= u <= 1:
2331        x = x1 + u * (x2 - x1)
2332        y = y1 + u * (y2 - y1)
2333        res = distance((x, y), p)
2334    else:
2335        res = False  # p is not between lp1 and lp2
2336
2337    return res

Given a point p and a line segment defined by boundary points lp1 and lp2, returns the distance between the line segment and the point. If the point is not located in the perpendicular area between the boundary points, returns False.

Arguments:
  • p (Point): Input point.
  • lp1 (Point): First boundary point of the line segment.
  • lp2 (Point): Second boundary point of the line segment.
Returns:

float: Distance between the point and the line segment, or False if the point is not in the perpendicular area.

def rotate_point( point: Sequence[float], angle: float, center: Sequence[float] = (0, 0)):
2953def rotate_point(point:Point, angle:float, center:Point=(0, 0)):
2954    """Rotate a point around a center by an angle in radians.
2955
2956    Args:
2957        point (Point): Point to rotate.
2958        angle (float): Angle of rotation in radians.
2959        center (Point): Center of rotation.
2960
2961    Returns:
2962        Point: Rotated point.
2963    """
2964    x, y = point[:2]
2965    cx, cy = center[:2]
2966    dx = x - cx
2967    dy = y - cy
2968    x = cx + dx * cos(angle) - dy * sin(angle)
2969    y = cy + dx * sin(angle) + dy * cos(angle)
2970    return (x, y)

Rotate a point around a center by an angle in radians.

Arguments:
  • point (Point): Point to rotate.
  • angle (float): Angle of rotation in radians.
  • center (Point): Center of rotation.
Returns:

Point: Rotated point.

def point_to_line_vec( point: Sequence[float], line: Sequence[Sequence], unit: bool = False) -> Sequence[float]:
2353def point_to_line_vec(point: Point, line: Line, unit: bool = False) -> VecType:
2354    """Return the perpendicular vector from a point to a line
2355
2356    Args:
2357        point (Point): Input point.
2358        line (Line): Input line.
2359        unit (bool, optional): Whether to return a unit vector. Defaults to False.
2360
2361    Returns:
2362        VecType: Perpendicular vector from the point to the line.
2363    """
2364    x0, y0 = point
2365    x1, y1 = line[0]
2366    x2, y2 = line[1]
2367    dx = x2 - x1
2368    dy = y2 - y1
2369    norm_ = sqrt(dx**2 + dy**2)
2370    unit_vec = [-dy / norm_, dx / norm_]
2371    dist = (dx * (y1 - y0) - (x1 - x0) * dy) / sqrt(dx**2 + dy**2)
2372    if unit:
2373        if dist > 0:
2374            res = [unit_vec[0], unit_vec[1]]
2375        else:
2376            res = [-unit_vec[0], -unit_vec[1]]
2377    else:
2378        res = [unit_vec[0] * dist, unit_vec[1] * dist]
2379
2380    return res

Return the perpendicular vector from a point to a line

Arguments:
  • point (Point): Input point.
  • line (Line): Input line.
  • unit (bool, optional): Whether to return a unit vector. Defaults to False.
Returns:

VecType: Perpendicular vector from the point to the line.

def polygon_area(polygon: Sequence[Sequence[float]], dist_tol=None) -> float:
2383def polygon_area(polygon: Sequence[Point], dist_tol=None) -> float:
2384    """Calculate the area of a polygon.
2385
2386    Args:
2387        polygon (Sequence[Point]): List of points representing the polygon.
2388        dist_tol (float, optional): Distance tolerance. Defaults to None.
2389
2390    Returns:
2391        float: Area of the polygon.
2392    """
2393    if dist_tol is None:
2394        dist_tol = defaults["dist_tol"]
2395    dist_tol2 = dist_tol * dist_tol
2396    if not close_points2(polygon[0], polygon[-1], dist2=dist_tol2):
2397        polygon = list(polygon[:])
2398        polygon.append(polygon[0])
2399    area_ = 0
2400    for i, point in enumerate(polygon[:-1]):
2401        x1, y1 = point
2402        x2, y2 = polygon[i + 1]
2403        area_ += x1 * y2 - x2 * y1
2404    return area_ / 2

Calculate the area of a polygon.

Arguments:
  • polygon (Sequence[Point]): List of points representing the polygon.
  • dist_tol (float, optional): Distance tolerance. Defaults to None.
Returns:

float: Area of the polygon.

def polyline_length(polygon: Sequence[Sequence[float]], closed=False, dist_tol=None) -> float:
2407def polyline_length(polygon: Sequence[Point], closed=False, dist_tol=None) -> float:
2408    """Calculate the perimeter of a polygon.
2409
2410    Args:
2411        polygon (Sequence[Point]): List of points representing the polygon.
2412        closed (bool, optional): Whether the polygon is closed. Defaults to False.
2413        dist_tol (float, optional): Distance tolerance. Defaults to None.
2414
2415    Returns:
2416        float: Perimeter of the polygon.
2417    """
2418    if dist_tol is None:
2419        dist_tol = defaults["dist_tol"]
2420    dist_tol2 = dist_tol * dist_tol
2421    if closed:
2422        if not close_points2(polygon[0], polygon[-1], dist2=dist_tol2):
2423            polygon = polygon[:]
2424            polygon.append(polygon[0])
2425    perimeter = 0
2426    for i, point in enumerate(polygon[:-1]):
2427        perimeter += distance(point, polygon[i + 1])
2428    return perimeter

Calculate the perimeter of a polygon.

Arguments:
  • polygon (Sequence[Point]): List of points representing the polygon.
  • closed (bool, optional): Whether the polygon is closed. Defaults to False.
  • dist_tol (float, optional): Distance tolerance. Defaults to None.
Returns:

float: Perimeter of the polygon.

def right_handed(polygon: Sequence[Sequence[float]], dist_tol=None) -> float:
2431def right_handed(polygon: Sequence[Point], dist_tol=None) -> float:
2432    """If polygon is counter-clockwise, return True
2433
2434    Args:
2435        polygon (Sequence[Point]): List of points representing the polygon.
2436        dist_tol (float, optional): Distance tolerance. Defaults to None.
2437
2438    Returns:
2439        bool: True if the polygon is counter-clockwise, False otherwise.
2440    """
2441    if dist_tol is None:
2442        dist_tol = defaults["dist_tol"]
2443    dist_tol2 = dist_tol * dist_tol
2444    added_point = False
2445    if not close_points2(polygon[0], polygon[-1], dist2=dist_tol2):
2446        polygon.append(polygon[0])
2447        added_point = True
2448    area_ = 0
2449    for i, point in enumerate(polygon[:-1]):
2450        x1, y1 = point
2451        x2, y2 = polygon[i + 1]
2452        area_ += x1 * y2 - x2 * y1
2453    if added_point:
2454        polygon.pop()
2455    return area_ > 0

If polygon is counter-clockwise, return True

Arguments:
  • polygon (Sequence[Point]): List of points representing the polygon.
  • dist_tol (float, optional): Distance tolerance. Defaults to None.
Returns:

bool: True if the polygon is counter-clockwise, False otherwise.

def radius2side_len(n: int, radius: float) -> float:
2458def radius2side_len(n: int, radius: float) -> float:
2459    """Given a radius and the number of sides, return the side length
2460    of an n-sided regular polygon with the given radius
2461
2462    Args:
2463        n (int): Number of sides.
2464        radius (float): Radius of the polygon.
2465
2466    Returns:
2467        float: Side length of the polygon.
2468    """
2469    return 2 * radius * sin(pi / n)

Given a radius and the number of sides, return the side length of an n-sided regular polygon with the given radius

Arguments:
  • n (int): Number of sides.
  • radius (float): Radius of the polygon.
Returns:

float: Side length of the polygon.

def tokenize_svg_path(path: str) -> list[str]:
2472def tokenize_svg_path(path: str) -> list[str]:
2473    """Tokenize an SVG path string.
2474
2475    Args:
2476        path (str): SVG path string.
2477
2478    Returns:
2479        list[str]: List of tokens.
2480    """
2481    return re.findall(r"[a-zA-Z]|[-+]?\d*\.\d+|\d+", path)

Tokenize an SVG path string.

Arguments:
  • path (str): SVG path string.
Returns:

list[str]: List of tokens.

def law_of_cosines(a: float, b: float, c: float) -> float:
2484def law_of_cosines(a: float, b: float, c: float) -> float:
2485    """Return the angle of a triangle given the three sides.
2486    Returns the angle of A in radians. A is the angle between
2487    sides b and c.
2488    cos(A) = (b^2 + c^2 - a^2) / (2 * b * c)
2489
2490    Args:
2491        a (float): Length of side a.
2492        b (float): Length of side b.
2493        c (float): Length of side c.
2494
2495    Returns:
2496        float: Angle of A in radians.
2497    """
2498    return acos((b**2 + c**2 - a**2) / (2 * b * c))

Return the angle of a triangle given the three sides. Returns the angle of A in radians. A is the angle between sides b and c. cos(A) = (b^2 + c^2 - a^2) / (2 * b * c)

Arguments:
  • a (float): Length of side a.
  • b (float): Length of side b.
  • c (float): Length of side c.
Returns:

float: Angle of A in radians.

def segmentize_catmull_rom(a: float, b: float, c: float, d: float, n: int = 100) -> Sequence[float]:
2501def segmentize_catmull_rom(
2502    a: float, b: float, c: float, d: float, n: int = 100
2503) -> Sequence[float]:
2504    """a and b are the control points and c and d are
2505    start and end points respectively,
2506    n is the number of segments to generate.
2507
2508    Args:
2509        a (float): First control point.
2510        b (float): Second control point.
2511        c (float): Start point.
2512        d (float): End point.
2513        n (int, optional): Number of segments to generate. Defaults to 100.
2514
2515    Returns:
2516        Sequence[float]: List of points representing the segments.
2517    """
2518    a = array(a[:2], dtype=float)
2519    b = array(b[:2], dtype=float)
2520    c = array(c[:2], dtype=float)
2521    d = array(d[:2], dtype=float)
2522
2523    t = 0
2524    dt = 1.0 / n
2525    points = []
2526    term1 = 2 * b
2527    term2 = -a + c
2528    term3 = 2 * a - 5 * b + 4 * c - d
2529    term4 = -a + 3 * b - 3 * c + d
2530
2531    for _ in range(n + 1):
2532        q = 0.5 * (term1 + term2 * t + term3 * t**2 + term4 * t**3)
2533        points.append([q[0], q[1]])
2534        t += dt
2535    return points

a and b are the control points and c and d are start and end points respectively, n is the number of segments to generate.

Arguments:
  • a (float): First control point.
  • b (float): Second control point.
  • c (float): Start point.
  • d (float): End point.
  • n (int, optional): Number of segments to generate. Defaults to 100.
Returns:

Sequence[float]: List of points representing the segments.

def side_len_to_radius(n: int, side_len: float) -> float:
2538def side_len_to_radius(n: int, side_len: float) -> float:
2539    """Given a side length and the number of sides, return the radius
2540    of an n-sided regular polygon with the given side_len length
2541
2542    Args:
2543        n (int): Number of sides.
2544        side_len (float): Side length of the polygon.
2545
2546    Returns:
2547        float: Radius of the polygon.
2548    """
2549    return side_len / (2 * sin(pi / n))

Given a side length and the number of sides, return the radius of an n-sided regular polygon with the given side_len length

Arguments:
  • n (int): Number of sides.
  • side_len (float): Side length of the polygon.
Returns:

float: Radius of the polygon.

def translate_line(dx: float, dy: float, line: Sequence[Sequence]) -> Sequence[Sequence]:
2552def translate_line(dx: float, dy: float, line: Line) -> Line:
2553    """Return a translated line by dx and dy
2554
2555    Args:
2556        dx (float): Translation distance in x-direction.
2557        dy (float): Translation distance in y-direction.
2558        line (Line): Input line.
2559
2560    Returns:
2561        Line: Translated line.
2562    """
2563    x1, y1 = line[0]
2564    x2, y2 = line[1]
2565    return [[x1 + dx, y1 + dy], [x2 + dx, y2 + dy]]

Return a translated line by dx and dy

Arguments:
  • dx (float): Translation distance in x-direction.
  • dy (float): Translation distance in y-direction.
  • line (Line): Input line.
Returns:

Line: Translated line.

def trim_line( line1: Sequence[Sequence], line2: Sequence[Sequence]) -> Sequence[Sequence]:
2568def trim_line(line1: Line, line2: Line) -> Line:
2569    """Trim line1 to the intersection of line1 and line2.
2570    Extend it if necessary.
2571
2572    Args:
2573        line1 (Line): First line.
2574        line2 (Line): Second line.
2575
2576    Returns:
2577        Line: Trimmed line.
2578    """
2579    intersection_ = intersection(line1, line2)
2580    return [line1[0], intersection_]

Trim line1 to the intersection of line1 and line2. Extend it if necessary.

Arguments:
  • line1 (Line): First line.
  • line2 (Line): Second line.
Returns:

Line: Trimmed line.

def unit_vector(line: Sequence[Sequence]) -> Sequence[float]:
2583def unit_vector(line: Line) -> VecType:
2584    """Return the unit vector of a line
2585
2586    Args:
2587        line (Line): Input line.
2588
2589    Returns:
2590        VecType: Unit vector of the line.
2591    """
2592    norm_ = length(line)
2593    p1, p2 = line
2594    x1, y1 = p1
2595    x2, y2 = p2
2596    return [(x2 - x1) / norm_, (y2 - y1) / norm_]

Return the unit vector of a line

Arguments:
  • line (Line): Input line.
Returns:

VecType: Unit vector of the line.

def unit_vector_(line: Sequence[Sequence]) -> Sequence[Sequence[float]]:
2599def unit_vector_(line: Line) -> Sequence[VecType]:
2600    """Return the cartesian unit vector of a line
2601    with the given line's start and end points
2602
2603    Args:
2604        line (Line): Input line.
2605
2606    Returns:
2607        Sequence[VecType]: Cartesian unit vector of the line.
2608    """
2609    x1, y1 = line[0]
2610    x2, y2 = line[1]
2611    dx = x2 - x1
2612    dy = y2 - y1
2613    norm_ = sqrt(dx**2 + dy**2)
2614    return [dx / norm_, dy / norm_]

Return the cartesian unit vector of a line with the given line's start and end points

Arguments:
  • line (Line): Input line.
Returns:

Sequence[VecType]: Cartesian unit vector of the line.

def vec_along_line(line: Sequence[Sequence], magnitude: float) -> Sequence[float]:
2617def vec_along_line(line: Line, magnitude: float) -> VecType:
2618    """Return a vector along a line with the given magnitude.
2619
2620    Args:
2621        line (Line): Input line.
2622        magnitude (float): Magnitude of the vector.
2623
2624    Returns:
2625        VecType: Vector along the line with the given magnitude.
2626    """
2627    if line == axis_x:
2628        dx, dy = magnitude, 0
2629    elif line == axis_y:
2630        dx, dy = 0, magnitude
2631    else:
2632        # line is (p1, p2)
2633        theta = line_angle(*line)
2634        dx = magnitude * cos(theta)
2635        dy = magnitude * sin(theta)
2636    return dx, dy

Return a vector along a line with the given magnitude.

Arguments:
  • line (Line): Input line.
  • magnitude (float): Magnitude of the vector.
Returns:

VecType: Vector along the line with the given magnitude.

def vec_dir_angle(vec: Sequence[float]) -> float:
2639def vec_dir_angle(vec: Sequence[float]) -> float:
2640    """Return the direction angle of a vector
2641
2642    Args:
2643        vec (Sequence[float]): Input vector.
2644
2645    Returns:
2646        float: Direction angle of the vector.
2647    """
2648    return atan2(vec[1], vec[0])

Return the direction angle of a vector

Arguments:
  • vec (Sequence[float]): Input vector.
Returns:

float: Direction angle of the vector.

def cross_product_sense(a: Sequence[float], b: Sequence[float], c: Sequence[float]) -> int:
2651def cross_product_sense(a: Point, b: Point, c: Point) -> int:
2652    """Return the cross product sense of vectors a and b.
2653
2654    Args:
2655        a (Point): First point.
2656        b (Point): Second point.
2657        c (Point): Third point.
2658
2659    Returns:
2660        int: Cross product sense.
2661    """
2662    length_ = cross_product2(a, b, c)
2663    if length_ == 0:
2664        res = 1
2665    else:
2666        res = length_ / abs(length)
2667
2668    return res

Return the cross product sense of vectors a and b.

Arguments:
  • a (Point): First point.
  • b (Point): Second point.
  • c (Point): Third point.
Returns:

int: Cross product sense.

def right_turn(p1, p2, p3):
2681def right_turn(p1, p2, p3):
2682    """Return True if p1, p2, p3 make a right turn.
2683
2684    Args:
2685        p1 (Point): First point.
2686        p2 (Point): Second point.
2687        p3 (Point): Third point.
2688
2689    Returns:
2690        bool: True if the points make a right turn, False otherwise.
2691    """
2692    return cross(p1, p2, p3) < 0

Return True if p1, p2, p3 make a right turn.

Arguments:
  • p1 (Point): First point.
  • p2 (Point): Second point.
  • p3 (Point): Third point.
Returns:

bool: True if the points make a right turn, False otherwise.

def left_turn(p1, p2, p3):
2695def left_turn(p1, p2, p3):
2696    """Return True if p1, p2, p3 make a left turn.
2697
2698    Args:
2699        p1 (Point): First point.
2700        p2 (Point): Second point.
2701        p3 (Point): Third point.
2702
2703    Returns:
2704        bool: True if the points make a left turn, False otherwise.
2705    """
2706    return cross(p1, p2, p3) > 0

Return True if p1, p2, p3 make a left turn.

Arguments:
  • p1 (Point): First point.
  • p2 (Point): Second point.
  • p3 (Point): Third point.
Returns:

bool: True if the points make a left turn, False otherwise.

def cross(p1, p2, p3):
2709def cross(p1, p2, p3):
2710    """Return the cross product of vectors p1p2 and p1p3.
2711
2712    Args:
2713        p1 (Point): First point.
2714        p2 (Point): Second point.
2715        p3 (Point): Third point.
2716
2717    Returns:
2718        float: Cross product of the vectors.
2719    """
2720    x1, y1 = p2[0] - p1[0], p2[1] - p1[1]
2721    x2, y2 = p3[0] - p1[0], p3[1] - p1[1]
2722    return x1 * y2 - x2 * y1

Return the cross product of vectors p1p2 and p1p3.

Arguments:
  • p1 (Point): First point.
  • p2 (Point): Second point.
  • p3 (Point): Third point.
Returns:

float: Cross product of the vectors.

def tri_to_cart(points):
2725def tri_to_cart(points):
2726    """
2727    Convert a list of points from triangular to cartesian coordinates.
2728
2729    Args:
2730        points (list[Point]): List of points in triangular coordinates.
2731
2732    Returns:
2733        np.ndarray: List of points in cartesian coordinates.
2734    """
2735    u = [1, 0]
2736    v = cos(pi / 3), sin(pi / 3)
2737    convert = array([u, v])
2738
2739    return array(points) @ convert

Convert a list of points from triangular to cartesian coordinates.

Arguments:
  • points (list[Point]): List of points in triangular coordinates.
Returns:

np.ndarray: List of points in cartesian coordinates.

def cart_to_tri(points):
2742def cart_to_tri(points):
2743    """
2744    Convert a list of points from cartesian to triangular coordinates.
2745
2746    Args:
2747        points (list[Point]): List of points in cartesian coordinates.
2748
2749    Returns:
2750        np.ndarray: List of points in triangular coordinates.
2751    """
2752    u = [1, 0]
2753    v = cos(pi / 3), sin(pi / 3)
2754    convert = np.linalg.inv(array([u, v]))
2755
2756    return array(points) @ convert

Convert a list of points from cartesian to triangular coordinates.

Arguments:
  • points (list[Point]): List of points in cartesian coordinates.
Returns:

np.ndarray: List of points in triangular coordinates.

def convex_hull(points):
2759def convex_hull(points):
2760    """Return the convex hull of a set of 2D points.
2761
2762    Args:
2763        points (list[Point]): List of 2D points.
2764
2765    Returns:
2766        list[Point]: Convex hull of the points.
2767    """
2768    # From http://en.wikibooks.org/wiki/Algorithm__implementation/Geometry/
2769    # Convex_hull/Monotone_chain
2770    # Sort points lexicographically (tuples are compared lexicographically).
2771    # Remove duplicates to detect the case we have just one unique point.
2772    points = sorted(set(points))
2773    # Boring case: no points or a single point, possibly repeated multiple times.
2774    if len(points) <= 1:
2775        return points
2776
2777    # 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross
2778    # product.
2779    # Return a positive value, if OAB makes a counter-clockwise turn,
2780    # negative for clockwise turn, and zero if the points are collinear.
2781    def cross_(o, a, b):
2782        return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
2783
2784    # Build lower hull
2785    lower = []
2786    for p in points:
2787        while len(lower) >= 2 and cross_(lower[-2], lower[-1], p) <= 0:
2788            lower.pop()
2789        lower.append(p)
2790    # Build upper hull
2791    upper = []
2792    for p in reversed(points):
2793        while len(upper) >= 2 and cross_(upper[-2], upper[-1], p) <= 0:
2794            upper.pop()
2795        upper.append(p)
2796    # Concatenation of the lower and upper hulls gives the convex hull.
2797    # Last point of each list is omitted because it is repeated at the beginning
2798    # of the other list.
2799    return lower[:-1] + upper[:-1]

Return the convex hull of a set of 2D points.

Arguments:
  • points (list[Point]): List of 2D points.
Returns:

list[Point]: Convex hull of the points.

def connected_pairs(items):
2802def connected_pairs(items):
2803    """Return a list of connected pair tuples corresponding to the items.
2804    [a, b, c] -> [(a, b), (b, c)]
2805
2806    Args:
2807        items (list): List of items.
2808
2809    Returns:
2810        list[tuple]: List of connected pair tuples.
2811    """
2812    return list(zip(items, items[1:]))

Return a list of connected pair tuples corresponding to the items. [a, b, c] -> [(a, b), (b, c)]

Arguments:
  • items (list): List of items.
Returns:

list[tuple]: List of connected pair tuples.

def flat_points(connected_segments):
2815def flat_points(connected_segments):
2816    """Return a list of points from a list of connected pairs of points.
2817
2818    Args:
2819        connected_segments (list[tuple]): List of connected pairs of points.
2820
2821    Returns:
2822        list[Point]: List of points.
2823    """
2824    points = [line[0] for line in connected_segments]
2825    points.append(connected_segments[-1][1])
2826    return points

Return a list of points from a list of connected pairs of points.

Arguments:
  • connected_segments (list[tuple]): List of connected pairs of points.
Returns:

list[Point]: List of points.

def point_in_quad(point: Sequence[float], quad: list[typing.Sequence[float]]) -> bool:
2829def point_in_quad(point: Point, quad: list[Point]) -> bool:
2830    """Return True if the point is inside the quad.
2831
2832    Args:
2833        point (Point): Input point.
2834        quad (list[Point]): List of points representing the quad.
2835
2836    Returns:
2837        bool: True if the point is inside the quad, False otherwise.
2838    """
2839    x, y = point[:2]
2840    x1, y1 = quad[0]
2841    x2, y2 = quad[1]
2842    x3, y3 = quad[2]
2843    x4, y4 = quad[3]
2844    xs = [x1, x2, x3, x4]
2845    ys = [y1, y2, y3, y4]
2846    min_x = min(xs)
2847    max_x = max(xs)
2848    min_y = min(ys)
2849    max_y = max(ys)
2850    return min_x <= x <= max_x and min_y <= y <= max_y

Return True if the point is inside the quad.

Arguments:
  • point (Point): Input point.
  • quad (list[Point]): List of points representing the quad.
Returns:

bool: True if the point is inside the quad, False otherwise.

def get_polygons( nested_points: Sequence[Sequence[float]], n_round_digits: int = 2, dist_tol: float = None) -> list:
2853def get_polygons(
2854    nested_points: Sequence[Point], n_round_digits: int = 2, dist_tol: float = None
2855) -> list:
2856    """Convert points to clean polygons. Points are vertices of polygons.
2857
2858    Args:
2859        nested_points (Sequence[Point]): List of nested points.
2860        n_round_digits (int, optional): Number of decimal places to round to. Defaults to 2.
2861        dist_tol (float, optional): Distance tolerance. Defaults to None.
2862
2863    Returns:
2864        list: List of clean polygons.
2865    """
2866    if dist_tol is None:
2867        dist_tol = defaults["dist_tol"]
2868    from ..graph import get_cycles
2869
2870    nested_rounded_points = []
2871    for points in nested_points:
2872        rounded_points = []
2873        for point in points:
2874            rounded_point = (around(point, n_round_digits)).tolist()
2875            rounded_points.append(tuple(rounded_point))
2876        nested_rounded_points.append(rounded_points)
2877
2878    s_points = set()
2879    d_id__point = {}
2880    d_point__id = {}
2881    for points in nested_rounded_points:
2882        for point in points:
2883            s_points.add(point)
2884
2885    for i, fs_point in enumerate(s_points):
2886        d_id__point[i] = fs_point  # we need a bidirectional dictionary
2887        d_point__id[fs_point] = i
2888
2889    nested_point_ids = []
2890    for points in nested_rounded_points:
2891        point_ids = []
2892        for point in points:
2893            point_ids.append(d_point__id[point])
2894        nested_point_ids.append(point_ids)
2895
2896    graph_edges = []
2897    for point_ids in nested_point_ids:
2898        graph_edges.extend(connected_pairs(point_ids))
2899    polygons = []
2900    graph_edges = sanitize_graph_edges(graph_edges)
2901    cycles = get_cycles(graph_edges)
2902    if cycles is None:
2903        return []
2904    for cycle_ in cycles:
2905        nodes = cycle_
2906        points = [d_id__point[i] for i in nodes]
2907        points = fix_degen_points(points, closed=True, dist_tol=dist_tol)
2908        polygons.append(points)
2909
2910    return polygons

Convert points to clean polygons. Points are vertices of polygons.

Arguments:
  • nested_points (Sequence[Point]): List of nested points.
  • n_round_digits (int, optional): Number of decimal places to round to. Defaults to 2.
  • dist_tol (float, optional): Distance tolerance. Defaults to None.
Returns:

list: List of clean polygons.

def offset_point_from_start(p1, p2, offset):
2913def offset_point_from_start(p1, p2, offset):
2914    """p1, p2: points on a line
2915    offset: distance from p1
2916    return the point on the line at the given offset
2917
2918    Args:
2919        p1 (Point): First point on the line.
2920        p2 (Point): Second point on the line.
2921        offset (float): Distance from p1.
2922
2923    Returns:
2924        Point: Point on the line at the given offset.
2925    """
2926    x1, y1 = p1
2927    x2, y2 = p2
2928    dx, dy = x2 - x1, y2 - y1
2929    d = (dx**2 + dy**2) ** 0.5
2930    if d == 0:
2931        res = p1
2932    else:
2933        res = (x1 + offset * dx / d, y1 + offset * dy / d)
2934
2935    return res

p1, p2: points on a line offset: distance from p1 return the point on the line at the given offset

Arguments:
  • p1 (Point): First point on the line.
  • p2 (Point): Second point on the line.
  • offset (float): Distance from p1.
Returns:

Point: Point on the line at the given offset.

def angle_between_two_lines(line1, line2):
2938def angle_between_two_lines(line1, line2):
2939    """Return the angle between two lines in radians.
2940
2941    Args:
2942        line1 (Line): First line.
2943        line2 (Line): Second line.
2944
2945    Returns:
2946        float: Angle between the two lines in radians.
2947    """
2948    alpha1 = line_angle(*line1)
2949    alpha2 = line_angle(*line2)
2950    return abs(alpha1 - alpha2)

Return the angle between two lines in radians.

Arguments:
  • line1 (Line): First line.
  • line2 (Line): Second line.
Returns:

float: Angle between the two lines in radians.

def circle_tangent_to2lines(line1, line2, intersection_, radius):
2973def circle_tangent_to2lines(line1, line2, intersection_, radius):
2974    """Given two lines, their intersection point and a radius,
2975    return the center of the circle tangent to both lines and
2976    with the given radius.
2977
2978    Args:
2979        line1 (Line): First line.
2980        line2 (Line): Second line.
2981        intersection_ (Point): Intersection point of the lines.
2982        radius (float): Radius of the circle.
2983
2984    Returns:
2985        tuple: Center of the circle, start and end points of the tangent lines.
2986    """
2987    alpha = angle_between_two_lines(line1, line2)
2988    dist = radius / sin(alpha / 2)
2989    start = offset_point_from_start(intersection_, line1.p1, dist)
2990    center = rotate_point(start, intersection_, alpha / 2)
2991    end = offset_point_from_start(intersection_, line2.p1, dist)
2992
2993    return center, start, end

Given two lines, their intersection point and a radius, return the center of the circle tangent to both lines and with the given radius.

Arguments:
  • line1 (Line): First line.
  • line2 (Line): Second line.
  • intersection_ (Point): Intersection point of the lines.
  • radius (float): Radius of the circle.
Returns:

tuple: Center of the circle, start and end points of the tangent lines.

def triangle_area(a: float, b: float, c: float) -> float:
2996def triangle_area(a: float, b: float, c: float) -> float:
2997    """
2998    Given side lengths a, b and c, return the area of the triangle.
2999
3000    Args:
3001        a (float): Length of side a.
3002        b (float): Length of side b.
3003        c (float): Length of side c.
3004
3005    Returns:
3006        float: Area of the triangle.
3007    """
3008    a_b = a - b
3009    return sqrt((a + (b + c)) * (c - (a_b)) * (c + (a_b)) * (a + (b - c))) / 4

Given side lengths a, b and c, return the area of the triangle.

Arguments:
  • a (float): Length of side a.
  • b (float): Length of side b.
  • c (float): Length of side c.
Returns:

float: Area of the triangle.

def round_point(point: list[float], n_digits: int = 2) -> list[float]:
3012def round_point(point: list[float], n_digits: int = 2) -> list[float]:
3013    """
3014    Round a point (x, y) to a given precision.
3015
3016    Args:
3017        point (list[float]): Input point.
3018        n_digits (int, optional): Number of decimal places to round to. Defaults to 2.
3019
3020    Returns:
3021        list[float]: Rounded point.
3022    """
3023    x, y = point[:2]
3024    x = round(x, n_digits)
3025    y = round(y, n_digits)
3026    return (x, y)

Round a point (x, y) to a given precision.

Arguments:
  • point (list[float]): Input point.
  • n_digits (int, optional): Number of decimal places to round to. Defaults to 2.
Returns:

list[float]: Rounded point.

def round_segment(segment: Sequence[Sequence[float]], n_digits: int = 2):
3029def round_segment(segment: Sequence[Point], n_digits: int = 2):
3030    """Round a segment to a given precision.
3031
3032    Args:
3033        segment (Sequence[Point]): Input segment.
3034        n_digits (int, optional): Number of decimal places to round to. Defaults to 2.
3035
3036    Returns:
3037        Sequence[Point]: Rounded segment.
3038    """
3039    p1 = round_point(segment[0], n_digits)
3040    p2 = round_point(segment[1], n_digits)
3041
3042    return [p1, p2]

Round a segment to a given precision.

Arguments:
  • segment (Sequence[Point]): Input segment.
  • n_digits (int, optional): Number of decimal places to round to. Defaults to 2.
Returns:

Sequence[Point]: Rounded segment.

def get_polygon_grid_point(n, line1, line2, circumradius=100):
3045def get_polygon_grid_point(n, line1, line2, circumradius=100):
3046    """See chapter ??? for explanation of this function.
3047
3048    Args:
3049        n (int): Number of sides.
3050        line1 (Line): First line.
3051        line2 (Line): Second line.
3052        circumradius (float, optional): Circumradius of the polygon. Defaults to 100.
3053
3054    Returns:
3055        Point: Grid point of the polygon.
3056    """
3057    s = circumradius * 2 * sin(pi / n)  # side length
3058    points = reg_poly_points(0, 0, n, s)[:-1]
3059    p1 = points[line1[0]]
3060    p2 = points[line1[1]]
3061    p3 = points[line2[0]]
3062    p4 = points[line2[1]]
3063
3064    return intersection((p1, p2), (p3, p4))[1]

See chapter ??? for explanation of this function.

Arguments:
  • n (int): Number of sides.
  • line1 (Line): First line.
  • line2 (Line): Second line.
  • circumradius (float, optional): Circumradius of the polygon. Defaults to 100.
Returns:

Point: Grid point of the polygon.

def congruent_polygons( polygon1: list[typing.Sequence[float]], polygon2: list[typing.Sequence[float]], dist_tol: float = None, area_tol: float = None, side_length_tol: float = None, angle_tol: float = None) -> bool:
3067def congruent_polygons(
3068    polygon1: list[Point],
3069    polygon2: list[Point],
3070    dist_tol: float = None,
3071    area_tol: float = None,
3072    side_length_tol: float = None,
3073    angle_tol: float = None,
3074) -> bool:
3075    """
3076    Return True if two polygons are congruent.
3077    They can be translated, rotated and/or reflected.
3078
3079    Args:
3080        polygon1 (list[Point]): First polygon.
3081        polygon2 (list[Point]): Second polygon.
3082        dist_tol (float, optional): Distance tolerance. Defaults to None.
3083        area_tol (float, optional): Area tolerance. Defaults to None.
3084        side_length_tol (float, optional): Side length tolerance. Defaults to None.
3085        angle_tol (float, optional): Angle tolerance. Defaults to None.
3086
3087    Returns:
3088        bool: True if the polygons are congruent, False otherwise.
3089    """
3090    dist_tol, area_tol, angle_tol = get_defaults(
3091        ["dist_tol", "area_rtol", "angle_rtol"], [dist_tol, area_tol, angle_tol]
3092    )
3093    if side_length_tol is None:
3094        side_length_tol = defaults["rtol"]
3095    dist_tol2 = dist_tol * dist_tol
3096    poly1 = polygon1
3097    poly2 = polygon2
3098    if close_points2(poly1[0], poly1[-1], dist2=dist_tol2):
3099        poly1 = poly1[:-1]
3100    if close_points2(poly2[0], poly2[-1], dist2=dist_tol2):
3101        poly2 = poly2[:-1]
3102    len_poly1 = len(poly1)
3103    len_poly2 = len(poly2)
3104    if len_poly1 != len_poly2:
3105        return False
3106    if not isclose(
3107        abs(polygon_area(poly1)), abs(polygon_area(poly2)), rtol=area_tol, atol=area_tol
3108    ):
3109        return False
3110
3111    side_lengths1 = [distance(poly1[i], poly1[i - 1]) for i in range(len_poly1)]
3112    side_lengths2 = [distance(poly2[i], poly2[i - 1]) for i in range(len_poly2)]
3113    check1 = equal_cycles(side_lengths1, side_lengths2, rtol=side_length_tol)
3114    if not check1:
3115        check_reverse = equal_cycles(
3116            side_lengths1, side_lengths2[::-1], rtol=side_length_tol
3117        )
3118        if not (check1 or check_reverse):
3119            return False
3120
3121    angles1 = polygon_internal_angles(poly1)
3122    angles2 = polygon_internal_angles(poly2)
3123    check1 = equal_cycles(angles1, angles2, angle_tol)
3124    if not check1:
3125        poly2 = poly2[::-1]
3126        angles2 = polygon_internal_angles(poly2)
3127        check_reverse = equal_cycles(angles1, angles2, angle_tol)
3128        if not (check1 or check_reverse):
3129            return False
3130
3131    return True

Return True if two polygons are congruent. They can be translated, rotated and/or reflected.

Arguments:
  • polygon1 (list[Point]): First polygon.
  • polygon2 (list[Point]): Second polygon.
  • dist_tol (float, optional): Distance tolerance. Defaults to None.
  • area_tol (float, optional): Area tolerance. Defaults to None.
  • side_length_tol (float, optional): Side length tolerance. Defaults to None.
  • angle_tol (float, optional): Angle tolerance. Defaults to None.
Returns:

bool: True if the polygons are congruent, False otherwise.

def positive_angle(angle, radians=True, tol=None, atol=None):
3134def positive_angle(angle, radians=True, tol=None, atol=None):
3135    """Return the positive angle in radians or degrees.
3136
3137    Args:
3138        angle (float): Input angle.
3139        radians (bool, optional): Whether the angle is in radians. Defaults to True.
3140        tol (float, optional): Relative tolerance. Defaults to None.
3141        atol (float, optional): Absolute tolerance. Defaults to None.
3142
3143    Returns:
3144        float: Positive angle.
3145    """
3146    tol, atol = get_defaults(["tol", "rtol"], [tol, atol])
3147    if radians:
3148        if angle < 0:
3149            angle += 2 * pi
3150        # if isclose(angle, TWO_PI, rtol=tol, atol=atol):
3151        #     angle = 0
3152    else:
3153        if angle < 0:
3154            angle += 360
3155        # if isclose(angle, 360, rtol=tol, atol=atol):
3156        #     angle = 0
3157    return angle

Return the positive angle in radians or degrees.

Arguments:
  • angle (float): Input angle.
  • radians (bool, optional): Whether the angle is in radians. Defaults to True.
  • tol (float, optional): Relative tolerance. Defaults to None.
  • atol (float, optional): Absolute tolerance. Defaults to None.
Returns:

float: Positive angle.

def polygon_internal_angles(polygon):
3160def polygon_internal_angles(polygon):
3161    """Return the internal angles of a polygon.
3162
3163    Args:
3164        polygon (list[Point]): List of points representing the polygon.
3165
3166    Returns:
3167        list[float]: List of internal angles of the polygon.
3168    """
3169    angles = []
3170    len_polygon = len(polygon)
3171    for i, pnt in enumerate(polygon):
3172        p1 = polygon[i - 1]
3173        p2 = pnt
3174        p3 = polygon[(i + 1) % len_polygon]
3175        angles.append(angle_between_lines2(p1, p2, p3))
3176
3177    return angles

Return the internal angles of a polygon.

Arguments:
  • polygon (list[Point]): List of points representing the polygon.
Returns:

list[float]: List of internal angles of the polygon.

def bisector_line( a: Sequence[float], b: Sequence[float], c: Sequence[float]) -> Sequence[Sequence]:
3180def bisector_line(a: Point, b: Point, c: Point) -> Line:
3181    """
3182    Given three points that form two lines [a, b] and [b, c]
3183    return the bisector line between them.
3184
3185    Args:
3186        a (Point): First point.
3187        b (Point): Second point.
3188        c (Point): Third point.
3189
3190    Returns:
3191        Line: Bisector line.
3192    """
3193    d = mid_point(a, c)
3194
3195    return [d, b]

Given three points that form two lines [a, b] and [b, c] return the bisector line between them.

Arguments:
  • a (Point): First point.
  • b (Point): Second point.
  • c (Point): Third point.
Returns:

Line: Bisector line.

def between(a, b, c):
3198def between(a, b, c):
3199    """Return True if c is between a and b.
3200
3201    Args:
3202        a (Point): First point.
3203        b (Point): Second point.
3204        c (Point): Third point.
3205
3206    Returns:
3207        bool: True if c is between a and b, False otherwise.
3208    """
3209    if not collinear(a, b, c):
3210        res = False
3211    elif a[0] != b[0]:
3212        res = ((a[0] <= c[0]) and (c[0] <= b[0])) or ((a[0] >= c[0]) and (c[0] >= b[0]))
3213    else:
3214        res = ((a[1] <= c[1]) and (c[1] <= b[1])) or ((a[1] >= c[1]) and (c[1] >= b[1]))
3215    return res

Return True if c is between a and b.

Arguments:
  • a (Point): First point.
  • b (Point): Second point.
  • c (Point): Third point.
Returns:

bool: True if c is between a and b, False otherwise.

def collinear(a, b, c, area_rtol=None, area_atol=None):
3218def collinear(a, b, c, area_rtol=None, area_atol=None):
3219    """Return True if a, b, and c are collinear.
3220
3221    Args:
3222        a (Point): First point.
3223        b (Point): Second point.
3224        c (Point): Third point.
3225        area_rtol (float, optional): Relative tolerance for area. Defaults to None.
3226        area_atol (float, optional): Absolute tolerance for area. Defaults to None.
3227
3228    Returns:
3229        bool: True if the points are collinear, False otherwise.
3230    """
3231    area_rtol, area_atol = get_defaults(
3232        ["area_rtol", "area_atol"], [area_rtol, area_atol]
3233    )
3234    return isclose(area(a, b, c), 0, rtol=area_rtol, atol=area_atol)

Return True if a, b, and c are collinear.

Arguments:
  • a (Point): First point.
  • b (Point): Second point.
  • c (Point): Third point.
  • area_rtol (float, optional): Relative tolerance for area. Defaults to None.
  • area_atol (float, optional): Absolute tolerance for area. Defaults to None.
Returns:

bool: True if the points are collinear, False otherwise.

def polar_to_cartesian(r, theta):
3237def polar_to_cartesian(r, theta):
3238    """Convert polar coordinates to cartesian coordinates.
3239
3240    Args:
3241        r (float): Radius.
3242        theta (float): Angle in radians.
3243
3244    Returns:
3245        Point: Cartesian coordinates.
3246    """
3247    return (r * cos(theta), r * sin(theta))

Convert polar coordinates to cartesian coordinates.

Arguments:
  • r (float): Radius.
  • theta (float): Angle in radians.
Returns:

Point: Cartesian coordinates.

def cartesian_to_polar(x, y):
3250def cartesian_to_polar(x, y):
3251    """Convert cartesian coordinates to polar coordinates.
3252
3253    Args:
3254        x (float): x-coordinate.
3255        y (float): y-coordinate.
3256
3257    Returns:
3258        tuple: Polar coordinates (r, theta).
3259    """
3260    r = hypot(x, y)
3261    theta = positive_angle(atan2(y, x))
3262    return r, theta

Convert cartesian coordinates to polar coordinates.

Arguments:
  • x (float): x-coordinate.
  • y (float): y-coordinate.
Returns:

tuple: Polar coordinates (r, theta).

def fillet( a: Sequence[float], b: Sequence[float], c: Sequence[float], radius: float) -> tuple[typing.Sequence[typing.Sequence], typing.Sequence[typing.Sequence], typing.Sequence[float]]:
3265def fillet(a: Point, b: Point, c: Point, radius: float) -> tuple[Line, Line, Point]:
3266    """
3267    Given three points that form two lines [a, b] and [b, c]
3268    return the clipped lines [a, d], [e, c], center point
3269    of the radius circle (tangent to both lines), and the arc
3270    angle of the formed fillet.
3271
3272    Args:
3273        a (Point): First point.
3274        b (Point): Second point.
3275        c (Point): Third point.
3276        radius (float): Radius of the fillet.
3277
3278    Returns:
3279        tuple: Clipped lines [a, d], [e, c], center point of the radius circle, and the arc angle.
3280    """
3281    alpha2 = angle_between_lines2(a, b, c) / 2
3282    sin_alpha2 = sin(alpha2)
3283    cos_alpha2 = cos(alpha2)
3284    clip_length = radius * cos_alpha2 / sin_alpha2
3285    d = offset_point_from_start(b, a, clip_length)
3286    e = offset_point_from_start(b, c, clip_length)
3287    mp = mid_point(a, c)  # [b, mp] is the bisector line
3288    center = offset_point_from_start(b, mp, radius / sin_alpha2)
3289    arc_angle = angle_between_lines2(e, center, d)
3290
3291    return [a, d], [e, c], center, arc_angle

Given three points that form two lines [a, b] and [b, c] return the clipped lines [a, d], [e, c], center point of the radius circle (tangent to both lines), and the arc angle of the formed fillet.

Arguments:
  • a (Point): First point.
  • b (Point): Second point.
  • c (Point): Third point.
  • radius (float): Radius of the fillet.
Returns:

tuple: Clipped lines [a, d], [e, c], center point of the radius circle, and the arc angle.

def line_by_point_angle_length(point, angle, length_):
3294def line_by_point_angle_length(point, angle, length_):
3295    """
3296    Given a point, an angle, and a length, return the line
3297    that starts at the point and has the given angle and length.
3298
3299    Args:
3300        point (Point): Start point of the line.
3301        angle (float): Angle of the line in radians.
3302        length_ (float): Length of the line.
3303
3304    Returns:
3305        Line: Line with the given angle and length.
3306    """
3307    x, y = point[:2]
3308    dx = length_ * cos(angle)
3309    dy = length_ * sin(angle)
3310
3311    return [(x, y), (x + dx, y + dy)]

Given a point, an angle, and a length, return the line that starts at the point and has the given angle and length.

Arguments:
  • point (Point): Start point of the line.
  • angle (float): Angle of the line in radians.
  • length_ (float): Length of the line.
Returns:

Line: Line with the given angle and length.

def surface_normal( p1: Sequence[float], p2: Sequence[float], p3: Sequence[float]) -> Sequence[float]:
3314def surface_normal(p1: Point, p2: Point, p3: Point) -> VecType:
3315    """
3316    Calculates the surface normal of a triangle given its vertices.
3317
3318    Args:
3319        p1 (Point): First vertex.
3320        p2 (Point): Second vertex.
3321        p3 (Point): Third vertex.
3322
3323    Returns:
3324        VecType: Surface normal vector.
3325    """
3326    v1 = np.array(p1)
3327    v2 = np.array(p2)
3328    v3 = np.array(p3)
3329    # Create two vectors from the vertices
3330    u = v2 - v1
3331    v = v3 - v1
3332
3333    # Calculate the cross product of the two vectors
3334    normal = np.cross(u, v)
3335
3336    # Normalize the vector to get a unit normal vector
3337    normal = normal / np.linalg.norm(normal)
3338
3339    return normal

Calculates the surface normal of a triangle given its vertices.

Arguments:
  • p1 (Point): First vertex.
  • p2 (Point): Second vertex.
  • p3 (Point): Third vertex.
Returns:

VecType: Surface normal vector.

def normal(point1, point2):
3342def normal(point1, point2):
3343    """Return the normal vector of a line.
3344
3345    Args:
3346        point1 (Point): First point of the line.
3347        point2 (Point): Second point of the line.
3348
3349    Returns:
3350        VecType: Normal vector of the line.
3351    """
3352    x1, y1 = point1
3353    x2, y2 = point2
3354    dx = x2 - x1
3355    dy = y2 - y1
3356    norm = sqrt(dx**2 + dy**2)
3357    return [-dy / norm, dx / norm]

Return the normal vector of a line.

Arguments:
  • point1 (Point): First point of the line.
  • point2 (Point): Second point of the line.
Returns:

VecType: Normal vector of the line.

def area(a, b, c):
3360def area(a, b, c):
3361    """Return the area of a triangle given its vertices.
3362
3363    Args:
3364        a (Point): First vertex.
3365        b (Point): Second vertex.
3366        c (Point): Third vertex.
3367
3368    Returns:
3369        float: Area of the triangle.
3370    """
3371    return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])

Return the area of a triangle given its vertices.

Arguments:
  • a (Point): First vertex.
  • b (Point): Second vertex.
  • c (Point): Third vertex.
Returns:

float: Area of the triangle.

def calc_area(points):
3374def calc_area(points):
3375    """Calculate the area of a simple polygon (given by a list of its vertices).
3376
3377    Args:
3378        points (list[Point]): List of points representing the polygon.
3379
3380    Returns:
3381        tuple: Area of the polygon and whether it is clockwise.
3382    """
3383    area_ = 0
3384    n_points = len(points)
3385    for i in range(n_points):
3386        v = points[i]
3387        vnext = points[(i + 1) % n_points]
3388        area_ += v[0] * vnext[1] - vnext[0] * v[1]
3389    clockwise = area_ > 0
3390
3391    return (abs(area_ / 2.0), clockwise)

Calculate the area of a simple polygon (given by a list of its vertices).

Arguments:
  • points (list[Point]): List of points representing the polygon.
Returns:

tuple: Area of the polygon and whether it is clockwise.

def remove_bad_points(points):
3394def remove_bad_points(points):
3395    """Remove redundant and collinear points from a list of points.
3396
3397    Args:
3398        points (list[Point]): List of points.
3399
3400    Returns:
3401        list[Point]: List of points with redundant and collinear points removed.
3402    """
3403    EPSILON = 1e-16
3404    n_points = len(points)
3405    # check for redundant points
3406    for i, p in enumerate(points[:]):
3407        for j in range(i + 1, n_points - 1):
3408            if p == points[j]:  # then remove the redundant point
3409                # maybe we should display a warning message here indicating
3410                # that redundant point is removed!!!
3411                points.remove(p)
3412
3413    n_points = len(points)
3414    # check for three consecutive points on a line
3415    lin_points = []
3416    for i in range(2, n_points - 1):
3417        if EPSILON > calc_area([points[i - 2], points[i - 1], points[i]])[0] > -EPSILON:
3418            lin_points.append(points[i - 1])
3419
3420    if EPSILON > calc_area([points[-2], points[-1], points[0]])[0] > -EPSILON:
3421        lin_points.append(points[-1])
3422
3423    for p in lin_points:
3424        # maybe we should display a warning message here indicating that linear
3425        # point is removed!!!
3426        points.remove(p)
3427
3428    return points

Remove redundant and collinear points from a list of points.

Arguments:
  • points (list[Point]): List of points.
Returns:

list[Point]: List of points with redundant and collinear points removed.

def is_convex(points):
3431def is_convex(points):
3432    """Return True if the polygon is convex.
3433
3434    Args:
3435        points (list[Point]): List of points representing the polygon.
3436
3437    Returns:
3438        bool: True if the polygon is convex, False otherwise.
3439    """
3440    points = remove_bad_points(points)
3441    n_checks = len(points)
3442    points = points + [points[0]]
3443    senses = []
3444    for i in range(n_checks):
3445        if i == (n_checks - 1):
3446            senses.append(cross_product_sense(points[i], points[0], points[1]))
3447        else:
3448            senses.append(cross_product_sense(points[i], points[i + 1], points[i + 2]))
3449    s = set(senses)
3450    return len(s) == 1

Return True if the polygon is convex.

Arguments:
  • points (list[Point]): List of points representing the polygon.
Returns:

bool: True if the polygon is convex, False otherwise.

def set_vertices(points):
3453def set_vertices(points):
3454    """Set the next and previous vertices of a list of vertices.
3455
3456    Args:
3457        points (list[Vertex]): List of vertices.
3458    """
3459    if not isinstance(points[0], Vertex):
3460        points = [Vertex(*p[:]) for p in points]
3461    n_points = len(points)
3462    for i, p in enumerate(points):
3463        if i == 0:
3464            p.prev = points[-1]
3465            p.next = points[i + 1]
3466        elif i == (n_points - 1):
3467            p.prev = points[i - 1]
3468            p.next = points[0]
3469        else:
3470            p.prev = points[i - 1]
3471            p.next = points[i + 1]
3472        p.angle = cross_product_sense(p.prev, p, p.next)

Set the next and previous vertices of a list of vertices.

Arguments:
  • points (list[Vertex]): List of vertices.
def circle_circle_intersections(x0, y0, r0, x1, y1, r1):
3475def circle_circle_intersections(x0, y0, r0, x1, y1, r1):
3476    """Return the intersection points of two circles.
3477
3478    Args:
3479        x0 (float): x-coordinate of the center of the first circle.
3480        y0 (float): y-coordinate of the center of the first circle.
3481        r0 (float): Radius of the first circle.
3482        x1 (float): x-coordinate of the center of the second circle.
3483        y1 (float): y-coordinate of the center of the second circle.
3484        r1 (float): Radius of the second circle.
3485
3486    Returns:
3487        tuple: Intersection points of the two circles.
3488    """
3489    # taken from https://stackoverflow.com/questions/55816902/finding-the-
3490    # intersection-of-two-circles
3491    # circle 1: (x0, y0), radius r0
3492    # circle 2: (x1, y1), radius r1
3493
3494    d = sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2)
3495
3496    # non intersecting
3497    if d > r0 + r1:
3498        res = None
3499    # One circle within other
3500    elif d < abs(r0 - r1):
3501        res = None
3502    # coincident circles
3503    elif d == 0 and r0 == r1:
3504        res = None
3505    else:
3506        a = (r0**2 - r1**2 + d**2) / (2 * d)
3507        h = sqrt(r0**2 - a**2)
3508        x2 = x0 + a * (x1 - x0) / d
3509        y2 = y0 + a * (x1 - x0) / d
3510        x3 = x2 + h * (y1 - y0) / d
3511        y3 = y2 - h * (x1 - x0) / d
3512        x4 = x2 - h * (y1 - y0) / d
3513        y4 = y2 + h * (x1 - x0) / d
3514
3515        res = (x3, y3, x4, y4)
3516
3517    return res

Return the intersection points of two circles.

Arguments:
  • x0 (float): x-coordinate of the center of the first circle.
  • y0 (float): y-coordinate of the center of the first circle.
  • r0 (float): Radius of the first circle.
  • x1 (float): x-coordinate of the center of the second circle.
  • y1 (float): y-coordinate of the center of the second circle.
  • r1 (float): Radius of the second circle.
Returns:

tuple: Intersection points of the two circles.

def circle_segment_intersection(circle, p1, p2):
3520def circle_segment_intersection(circle, p1, p2):
3521    """Return True if the circle and the line segment intersect.
3522
3523    Args:
3524        circle (Circle): Input circle.
3525        p1 (Point): First point of the line segment.
3526        p2 (Point): Second point of the line segment.
3527
3528    Returns:
3529        bool: True if the circle and the line segment intersect, False otherwise.
3530    """
3531    # if line seg and circle intersects returns true, false otherwise
3532    # c: circle
3533    # p1 and p2 are the endpoints of the line segment
3534
3535    x3, y3 = circle.pos[:2]
3536    x1, y1 = p1[:2]
3537    x2, y2 = p2[:2]
3538    if (
3539        distance(p1, circle.pos) < circle.radius
3540        or distance(p2, circle.pos) < circle.radius
3541    ):
3542        return True
3543    u = ((x3 - x1) * (x2 - x1) + (y3 - y1) * (y2 - y1)) / (
3544        (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)
3545    )
3546    res = False
3547    if 0 <= u <= 1:
3548        x = x1 + u * (x2 - x1)
3549        y = y1 + u * (y2 - y1)
3550        if distance((x, y), circle.pos) < circle.radius:
3551            res = True
3552
3553    return res  # p is not between lp1 and lp2

Return True if the circle and the line segment intersect.

Arguments:
  • circle (Circle): Input circle.
  • p1 (Point): First point of the line segment.
  • p2 (Point): Second point of the line segment.
Returns:

bool: True if the circle and the line segment intersect, False otherwise.

def r_polar(a, b, theta):
3556def r_polar(a, b, theta):
3557    """Return the radius (distance between the center and the intersection point)
3558    of the ellipse at the given angle.
3559
3560    Args:
3561        a (float): Semi-major axis of the ellipse.
3562        b (float): Semi-minor axis of the ellipse.
3563        theta (float): Angle in radians.
3564
3565    Returns:
3566        float: Radius of the ellipse at the given angle.
3567    """
3568    return (a * b) / sqrt((b * cos(theta)) ** 2 + (a * sin(theta)) ** 2)

Return the radius (distance between the center and the intersection point) of the ellipse at the given angle.

Arguments:
  • a (float): Semi-major axis of the ellipse.
  • b (float): Semi-minor axis of the ellipse.
  • theta (float): Angle in radians.
Returns:

float: Radius of the ellipse at the given angle.

def ellipse_line_intersection(a, b, point):
3571def ellipse_line_intersection(a, b, point):
3572    """Return the intersection points of an ellipse and a line segment
3573    connecting the given point to the ellipse center at (0, 0).
3574
3575    Args:
3576        a (float): Semi-major axis of the ellipse.
3577        b (float): Semi-minor axis of the ellipse.
3578        point (Point): Point on the line segment.
3579
3580    Returns:
3581        list[Point]: Intersection points of the ellipse and the line segment.
3582    """
3583    # adapted from http://mathworld.wolfram.com/Ellipse-LineIntersection.html
3584    # a, b is the ellipse width/2 and height/2 and (x_0, y_0) is the point
3585
3586    x_0, y_0 = point[:2]
3587    x = ((a * b) / (sqrt(a**2 * y_0**2 + b**2 * x_0**2))) * x_0
3588    y = ((a * b) / (sqrt(a**2 * y_0**2 + b**2 * x_0**2))) * y_0
3589
3590    return [(x, y), (-x, -y)]

Return the intersection points of an ellipse and a line segment connecting the given point to the ellipse center at (0, 0).

Arguments:
  • a (float): Semi-major axis of the ellipse.
  • b (float): Semi-minor axis of the ellipse.
  • point (Point): Point on the line segment.
Returns:

list[Point]: Intersection points of the ellipse and the line segment.

def ellipse_tangent(a, b, x, y, tol=0.001):
3593def ellipse_tangent(a, b, x, y, tol=0.001):
3594    """Calculates the slope of the tangent line to an ellipse at the point (x, y).
3595    If point is not on the ellipse, return False.
3596
3597    Args:
3598        a (float): Semi-major axis of the ellipse.
3599        b (float): Semi-minor axis of the ellipse.
3600        x (float): x-coordinate of the point.
3601        y (float): y-coordinate of the point.
3602        tol (float, optional): Tolerance. Defaults to 0.001.
3603
3604    Returns:
3605        float: Slope of the tangent line, or False if the point is not on the ellipse.
3606    """
3607    if abs((x**2 / a**2) + (y**2 / b**2) - 1) > tol:
3608        res = False
3609    else:
3610        res = -(b**2 * x) / (a**2 * y)
3611
3612    return res

Calculates the slope of the tangent line to an ellipse at the point (x, y). If point is not on the ellipse, return False.

Arguments:
  • a (float): Semi-major axis of the ellipse.
  • b (float): Semi-minor axis of the ellipse.
  • x (float): x-coordinate of the point.
  • y (float): y-coordinate of the point.
  • tol (float, optional): Tolerance. Defaults to 0.001.
Returns:

float: Slope of the tangent line, or False if the point is not on the ellipse.

def elliptic_arclength(t_0, t_1, a, b):
3615def elliptic_arclength(t_0, t_1, a, b):
3616    """Return the arclength of an ellipse between the given parametric angles.
3617    The ellipse has semi-major axis a and semi-minor axis b.
3618
3619    Args:
3620        t_0 (float): Start parametric angle in radians.
3621        t_1 (float): End parametric angle in radians.
3622        a (float): Semi-major axis of the ellipse.
3623        b (float): Semi-minor axis of the ellipse.
3624
3625    Returns:
3626        float: Arclength of the ellipse between the given parametric angles.
3627    """
3628    # from: https://www.johndcook.com/blog/2022/11/02/elliptic-arc-length/
3629    from scipy.special import ellipeinc # this takes too long to import!!!
3630    m = 1 - (b / a) ** 2
3631    t1 = ellipeinc(t_1 - 0.5 * pi, m)
3632    t0 = ellipeinc(t_0 - 0.5 * pi, m)
3633    return a * (t1 - t0)

Return the arclength of an ellipse between the given parametric angles. The ellipse has semi-major axis a and semi-minor axis b.

Arguments:
  • t_0 (float): Start parametric angle in radians.
  • t_1 (float): End parametric angle in radians.
  • a (float): Semi-major axis of the ellipse.
  • b (float): Semi-minor axis of the ellipse.
Returns:

float: Arclength of the ellipse between the given parametric angles.

def central_to_parametric_angle(a, b, phi):
3636def central_to_parametric_angle(a, b, phi):
3637    """
3638    Converts a central angle to a parametric angle on an ellipse.
3639
3640    Args:
3641        a (float): Semi-major axis of the ellipse.
3642        b (float): Semi-minor axis of the ellipse.
3643        phi (float): Central angle in radians.
3644
3645    Returns:
3646        float: Parametric angle in radians.
3647    """
3648    t = atan2((a / b) * sin(phi), cos(phi))
3649    if t < 0:
3650        t += 2 * pi
3651
3652    return t

Converts a central angle to a parametric angle on an ellipse.

Arguments:
  • a (float): Semi-major axis of the ellipse.
  • b (float): Semi-minor axis of the ellipse.
  • phi (float): Central angle in radians.
Returns:

float: Parametric angle in radians.

def parametric_to_central_angle(a, b, t):
3655def parametric_to_central_angle(a, b, t):
3656    """
3657    Converts a parametric angle on an ellipse to a central angle.
3658
3659    Args:
3660        a (float): Semi-major axis of the ellipse.
3661        b (float): Semi-minor axis of the ellipse.
3662        t (float): Parametric angle in radians.
3663
3664    Returns:
3665        float: Central angle in radians.
3666    """
3667    phi = atan2((b / a) * sin(t), cos(t))
3668    if phi < 0:
3669        phi += 2 * pi
3670
3671    return phi

Converts a parametric angle on an ellipse to a central angle.

Arguments:
  • a (float): Semi-major axis of the ellipse.
  • b (float): Semi-minor axis of the ellipse.
  • t (float): Parametric angle in radians.
Returns:

float: Central angle in radians.

def ellipse_points(center, a, b, n_points):
3674def ellipse_points(center, a, b, n_points):
3675    """Generate points on an ellipse.
3676
3677    Args:
3678        center (tuple): (x, y) coordinates of the ellipse center.
3679        a (float): Length of the semi-major axis.
3680        b (float): Length of the semi-minor axis.
3681        n_points (int): Number of points to generate.
3682
3683    Returns:
3684        np.ndarray: Array of (x, y) coordinates of the ellipse points.
3685    """
3686    t = np.linspace(0, 2 * np.pi, n_points)
3687    x = center[0] + a * np.cos(t)
3688    y = center[1] + b * np.sin(t)
3689
3690    return np.column_stack((x, y))

Generate points on an ellipse.

Arguments:
  • center (tuple): (x, y) coordinates of the ellipse center.
  • a (float): Length of the semi-major axis.
  • b (float): Length of the semi-minor axis.
  • n_points (int): Number of points to generate.
Returns:

np.ndarray: Array of (x, y) coordinates of the ellipse points.

def ellipse_point(a, b, angle):
3693def ellipse_point(a, b, angle):
3694    """Return a point on an ellipse with the given a=width/2, b=height/2, and angle.
3695
3696    Args:
3697        a (float): Semi-major axis of the ellipse.
3698        b (float): Semi-minor axis of the ellipse.
3699        angle (float): Angle in radians.
3700
3701    Returns:
3702        Point: Point on the ellipse.
3703    """
3704    r = r_polar(a, b, angle)
3705
3706    return (r * cos(angle), r * sin(angle))

Return a point on an ellipse with the given a=width/2, b=height/2, and angle.

Arguments:
  • a (float): Semi-major axis of the ellipse.
  • b (float): Semi-minor axis of the ellipse.
  • angle (float): Angle in radians.
Returns:

Point: Point on the ellipse.

def circle_line_intersection(c, p1, p2):
3709def circle_line_intersection(c, p1, p2):
3710    """Return the intersection points of a circle and a line segment.
3711
3712    Args:
3713        c (Circle): Input circle.
3714        p1 (Point): First point of the line segment.
3715        p2 (Point): Second point of the line segment.
3716
3717    Returns:
3718        tuple: Intersection points of the circle and the line segment.
3719    """
3720
3721    # adapted from http://mathworld.wolfram.com/Circle-LineIntersection.html
3722    # c is the circle and p1 and p2 are the line points
3723    def sgn(num):
3724        if num < 0:
3725            res = -1
3726        else:
3727            res = 1
3728        return res
3729
3730    x1, y1 = p1[:2]
3731    x2, y2 = p2[:2]
3732    r = c.radius
3733    x, y = c.pos[:2]
3734
3735    x1 -= x
3736    x2 -= x
3737    y1 -= y
3738    y2 -= y
3739
3740    dx = x2 - x1
3741    dy = y2 - y1
3742    dr = sqrt(dx**2 + dy**2)
3743    d = x1 * y2 - x2 * y1
3744    d2 = d**2
3745    r2 = r**2
3746    dr2 = dr**2
3747
3748    discriminant = r2 * dr2 - d2
3749
3750    if discriminant > 0:
3751        ddy = d * dy
3752        ddx = d * dx
3753        sqrterm = sqrt(r2 * dr2 - d2)
3754        temp = sgn(dy) * dx * sqrterm
3755
3756        a = (ddy + temp) / dr2
3757        b = (-ddx + abs(dy) * sqrterm) / dr2
3758        if discriminant == 0:
3759            res = (a + x, b + y)
3760        else:
3761            c = (ddy - temp) / dr2
3762            d = (-ddx - abs(dy) * sqrterm) / dr2
3763            res = ((a + x, b + y), (c + x, d + y))
3764
3765    else:
3766        res = False
3767
3768    return res

Return the intersection points of a circle and a line segment.

Arguments:
  • c (Circle): Input circle.
  • p1 (Point): First point of the line segment.
  • p2 (Point): Second point of the line segment.
Returns:

tuple: Intersection points of the circle and the line segment.

def circle_poly_intersection(circle, polygon):
3771def circle_poly_intersection(circle, polygon):
3772    """Return True if the circle and the polygon intersect.
3773
3774    Args:
3775        circle (Circle): Input circle.
3776        polygon (Polygon): Input polygon.
3777
3778    Returns:
3779        bool: True if the circle and the polygon intersect, False otherwise.
3780    """
3781    points = polygon.vertices
3782    n = len(points)
3783    res = False
3784    for i in range(n):
3785        x = points[i][0]
3786        y = points[i][1]
3787        x1 = points[(i + 1) % n][0]
3788        y1 = points[(i + 1) % n][1]
3789        if circle_segment_intersection(circle, (x, y), (x1, y1)):
3790            res = True
3791            break
3792    return res

Return True if the circle and the polygon intersect.

Arguments:
  • circle (Circle): Input circle.
  • polygon (Polygon): Input polygon.
Returns:

bool: True if the circle and the polygon intersect, False otherwise.

def point_to_circle_distance(point, center, radius):
3795def point_to_circle_distance(point, center, radius):
3796    """Given a point, center point, and radius, returns distance
3797    between the given point and the circle
3798
3799    Args:
3800        point (Point): Input point.
3801        center (Point): Center of the circle.
3802        radius (float): Radius of the circle.
3803
3804    Returns:
3805        float: Distance between the point and the circle.
3806    """
3807    return abs(distance(center, point) - radius)

Given a point, center point, and radius, returns distance between the given point and the circle

Arguments:
  • point (Point): Input point.
  • center (Point): Center of the circle.
  • radius (float): Radius of the circle.
Returns:

float: Distance between the point and the circle.

def get_interior_points(start, end, n_points):
3810def get_interior_points(start, end, n_points):
3811    """Given start and end points and number of interior points
3812    returns the positions of the interior points
3813
3814    Args:
3815        start (Point): Start point.
3816        end (Point): End point.
3817        n_points (int): Number of interior points.
3818
3819    Returns:
3820        list[Point]: List of interior points.
3821    """
3822    rot_angle = line_angle(start, end)
3823    length_ = distance(start, end)
3824    seg_length = length_ / (n_points + 1.0)
3825    points = []
3826    for i in range(n_points):
3827        points.append(
3828            rotate_point([start[0] + seg_length * (i + 1), start[1]], start, rot_angle)
3829        )
3830    return points

Given start and end points and number of interior points returns the positions of the interior points

Arguments:
  • start (Point): Start point.
  • end (Point): End point.
  • n_points (int): Number of interior points.
Returns:

list[Point]: List of interior points.

def circle_3point(point1, point2, point3):
3833def circle_3point(point1, point2, point3):
3834    """Given three points, returns the center point and radius
3835
3836    Args:
3837        point1 (Point): First point.
3838        point2 (Point): Second point.
3839        point3 (Point): Third point.
3840
3841    Returns:
3842        tuple: Center point and radius of the circle.
3843    """
3844    ax, ay = point1[:2]
3845    bx, by = point2[:2]
3846    cx, cy = point3[:2]
3847    a = bx - ax
3848    b = by - ay
3849    c = cx - ax
3850    d = cy - ay
3851    e = a * (ax + bx) + b * (ay + by)
3852    f = c * (ax + cx) + d * (ay + cy)
3853    g = 2.0 * (a * (cy - by) - b * (cx - bx))
3854    if g == 0:
3855        raise ValueError("Points are collinear!")
3856
3857    px = ((d * e) - (b * f)) / g
3858    py = ((a * f) - (c * e)) / g
3859    r = ((ax - px) ** 2 + (ay - py) ** 2) ** 0.5
3860    return ((px, py), r)

Given three points, returns the center point and radius

Arguments:
  • point1 (Point): First point.
  • point2 (Point): Second point.
  • point3 (Point): Third point.
Returns:

tuple: Center point and radius of the circle.

def project_point_on_line( point: Vertex, line: Edge):
3863def project_point_on_line(point: Vertex, line: Edge):
3864    """Given a point and a line, returns the projection of the point on the line
3865
3866    Args:
3867        point (Vertex): Input point.
3868        line (Edge): Input line.
3869
3870    Returns:
3871        Vertex: Projection of the point on the line.
3872    """
3873    v = point
3874    a, b = line
3875
3876    av = v - a
3877    ab = b - a
3878    t = (av * ab) / (ab * ab)
3879    if t < 0.0:
3880        t = 0.0
3881    elif t > 1.0:
3882        t = 1.0
3883    return a + ab * t

Given a point and a line, returns the projection of the point on the line

Arguments:
  • point (Vertex): Input point.
  • line (Edge): Input line.
Returns:

Vertex: Projection of the point on the line.

class Vertex(builtins.list):
3886class Vertex(list):
3887    """A 3D vertex."""
3888
3889    def __init__(self, x, y, z=0):
3890        self.x = x
3891        self.y = y
3892        self.z = z
3893        self.type = Types.VERTEX
3894        common_properties(self, graphics_object=False)
3895
3896    def __repr__(self):
3897        return f"Vertex({self.x}, {self.y}, {self.z})"
3898
3899    def __eq__(self, other):
3900        return self[0] == other[0] and self[1] == other[1] and self[2] == other[2]
3901
3902    def copy(self):
3903        return Vertex(self.x, self.y, self.z)
3904
3905    def __add__(self, other):
3906        return Vertex(self.x + other.x, self.y + other.y, self.z + other.z)
3907
3908    def __sub__(self, other):
3909        return Vertex(self.x - other.x, self.y - other.y, self.z - other.z)
3910
3911    @property
3912    def coords(self):
3913        """Return the coordinates as a tuple."""
3914        return (self.x, self.y, self.z)
3915
3916    @property
3917    def array(self):
3918        """Homogeneous coordinates as a numpy array."""
3919        return array([self.x, self.y, 1])
3920
3921    def v_tuple(self):
3922        """Return the vertex as a tuple."""
3923        return (self.x, self.y, self.z)
3924
3925    def below(self, other):
3926        """This is for 2D points only
3927
3928        Args:
3929            other (Vertex): Other vertex.
3930
3931        Returns:
3932            bool: True if this vertex is below the other vertex, False otherwise.
3933        """
3934        res = False
3935        if self.y < other.y:
3936            res = True
3937        elif self.y == other.y:
3938            if self.x > other.x:
3939                res = True
3940        return res
3941
3942    def above(self, other):
3943        """This is for 2D points only
3944
3945        Args:
3946            other (Vertex): Other vertex.
3947
3948        Returns:
3949            bool: True if this vertex is above the other vertex, False otherwise.
3950        """
3951        if self.y > other.y:
3952            res = True
3953        elif self.y == other.y and self.x < other.x:
3954            res = True
3955        else:
3956            res = False
3957
3958        return res

A 3D vertex.

Vertex(x, y, z=0)
3889    def __init__(self, x, y, z=0):
3890        self.x = x
3891        self.y = y
3892        self.z = z
3893        self.type = Types.VERTEX
3894        common_properties(self, graphics_object=False)
x
y
z
type
def copy(self):
3902    def copy(self):
3903        return Vertex(self.x, self.y, self.z)

Return a shallow copy of the list.

coords
3911    @property
3912    def coords(self):
3913        """Return the coordinates as a tuple."""
3914        return (self.x, self.y, self.z)

Return the coordinates as a tuple.

array
3916    @property
3917    def array(self):
3918        """Homogeneous coordinates as a numpy array."""
3919        return array([self.x, self.y, 1])

Homogeneous coordinates as a numpy array.

def v_tuple(self):
3921    def v_tuple(self):
3922        """Return the vertex as a tuple."""
3923        return (self.x, self.y, self.z)

Return the vertex as a tuple.

def below(self, other):
3925    def below(self, other):
3926        """This is for 2D points only
3927
3928        Args:
3929            other (Vertex): Other vertex.
3930
3931        Returns:
3932            bool: True if this vertex is below the other vertex, False otherwise.
3933        """
3934        res = False
3935        if self.y < other.y:
3936            res = True
3937        elif self.y == other.y:
3938            if self.x > other.x:
3939                res = True
3940        return res

This is for 2D points only

Arguments:
  • other (Vertex): Other vertex.
Returns:

bool: True if this vertex is below the other vertex, False otherwise.

def above(self, other):
3942    def above(self, other):
3943        """This is for 2D points only
3944
3945        Args:
3946            other (Vertex): Other vertex.
3947
3948        Returns:
3949            bool: True if this vertex is above the other vertex, False otherwise.
3950        """
3951        if self.y > other.y:
3952            res = True
3953        elif self.y == other.y and self.x < other.x:
3954            res = True
3955        else:
3956            res = False
3957
3958        return res

This is for 2D points only

Arguments:
  • other (Vertex): Other vertex.
Returns:

bool: True if this vertex is above the other vertex, False otherwise.

class Edge:
3961class Edge:
3962    """A 2D edge."""
3963
3964    def __init__(
3965        self, start_point: Union[Point, Vertex], end_point: Union[Point, Vertex]
3966    ):
3967        if isinstance(start_point, Point):
3968            start = Vertex(*start_point)
3969        elif isinstance(end_point, Vertex):
3970            start = start_point
3971        else:
3972            raise ValueError("Start point should be a Point or Vertex instance.")
3973
3974        if isinstance(end_point, Point):
3975            end = Vertex(*end_point)
3976        elif isinstance(end_point, Vertex):
3977            end = end_point
3978        else:
3979            raise ValueError("End point should be a Point or Vertex instance.")
3980
3981        self.start = start
3982        self.end = end
3983        self.type = Types.EDGE
3984        common_properties(self, graphics_object=False)
3985
3986    def __repr__(self):
3987        return str(f"Edge({self.start}, {self.end})")
3988
3989    def __str__(self):
3990        return str(f"Edge({self.start.point}, {self.end.point})")
3991
3992    def __eq__(self, other):
3993        start = other.start.point
3994        end = other.end.point
3995
3996        return (
3997            isclose(
3998                self.start.point, start, rtol=defaults["rtol"], atol=defaults["atol"]
3999            )
4000            and isclose(
4001                self.end.point, end, rtol=defaults["rtol"], atol=defaults["atol"]
4002            )
4003        ) or (
4004            isclose(self.start.point, end, rtol=defaults["rtol"], atol=defaults["atol"])
4005            and isclose(
4006                self.end.point, start, rtol=defaults["rtol"], atol=defaults["atol"]
4007            )
4008        )
4009
4010    def __getitem__(self, subscript):
4011        vertices = self.vertices
4012        if isinstance(subscript, slice):
4013            res = vertices[subscript.start : subscript.stop : subscript.step]
4014        elif isinstance(subscript, int):
4015            res = vertices[subscript]
4016        else:
4017            raise ValueError("Invalid subscript.")
4018        return res
4019
4020    def __setitem__(self, subscript, value):
4021        vertices = self.vertices
4022        if isinstance(subscript, slice):
4023            vertices[subscript.start : subscript.stop : subscript.step] = value
4024        else:
4025            isinstance(subscript, int)
4026            vertices[subscript] = value
4027
4028    @property
4029    def slope(self):
4030        """Line slope. The slope of the line passing through the start and end points."""
4031        return (self.y2 - self.y1) / (self.x2 - self.x1)
4032
4033    @property
4034    def angle(self):
4035        """Line angle. Angle between the line and the x-axis."""
4036        return atan2(self.y2 - self.y1, self.x2 - self.x1)
4037
4038    @property
4039    def inclination(self):
4040        """Inclination angle. Angle between the line and the x-axis converted to
4041        a value between zero and pi."""
4042        return self.angle % pi
4043
4044    @property
4045    def length(self):
4046        """Length of the line segment."""
4047        return distance(self.start.point, self.end.point)
4048
4049    @property
4050    def x1(self):
4051        """x-coordinate of the start point."""
4052        return self.start.x
4053
4054    @property
4055    def y1(self):
4056        """y-coordinate of the start point."""
4057        return self.start.y
4058
4059    @property
4060    def x2(self):
4061        """x-coordinate of the end point."""
4062        return self.end.x
4063
4064    @property
4065    def y2(self):
4066        """y-coordinate of the end point."""
4067        return self.end.y
4068
4069    @property
4070    def points(self):
4071        """Start and end"""
4072        return [self.start.point, self.end.point]
4073
4074    @property
4075    def vertices(self):
4076        """Start and end vertices."""
4077        return [self.start, self.end]
4078
4079    @property
4080    def array(self):
4081        """Homogeneous coordinates as a numpy array."""
4082        return array([self.start.array, self.end.array])

A 2D edge.

Edge( start_point: Union[Sequence[float], Vertex], end_point: Union[Sequence[float], Vertex])
3964    def __init__(
3965        self, start_point: Union[Point, Vertex], end_point: Union[Point, Vertex]
3966    ):
3967        if isinstance(start_point, Point):
3968            start = Vertex(*start_point)
3969        elif isinstance(end_point, Vertex):
3970            start = start_point
3971        else:
3972            raise ValueError("Start point should be a Point or Vertex instance.")
3973
3974        if isinstance(end_point, Point):
3975            end = Vertex(*end_point)
3976        elif isinstance(end_point, Vertex):
3977            end = end_point
3978        else:
3979            raise ValueError("End point should be a Point or Vertex instance.")
3980
3981        self.start = start
3982        self.end = end
3983        self.type = Types.EDGE
3984        common_properties(self, graphics_object=False)
start
end
type
slope
4028    @property
4029    def slope(self):
4030        """Line slope. The slope of the line passing through the start and end points."""
4031        return (self.y2 - self.y1) / (self.x2 - self.x1)

Line slope. The slope of the line passing through the start and end points.

angle
4033    @property
4034    def angle(self):
4035        """Line angle. Angle between the line and the x-axis."""
4036        return atan2(self.y2 - self.y1, self.x2 - self.x1)

Line angle. Angle between the line and the x-axis.

inclination
4038    @property
4039    def inclination(self):
4040        """Inclination angle. Angle between the line and the x-axis converted to
4041        a value between zero and pi."""
4042        return self.angle % pi

Inclination angle. Angle between the line and the x-axis converted to a value between zero and pi.

length
4044    @property
4045    def length(self):
4046        """Length of the line segment."""
4047        return distance(self.start.point, self.end.point)

Length of the line segment.

x1
4049    @property
4050    def x1(self):
4051        """x-coordinate of the start point."""
4052        return self.start.x

x-coordinate of the start point.

y1
4054    @property
4055    def y1(self):
4056        """y-coordinate of the start point."""
4057        return self.start.y

y-coordinate of the start point.

x2
4059    @property
4060    def x2(self):
4061        """x-coordinate of the end point."""
4062        return self.end.x

x-coordinate of the end point.

y2
4064    @property
4065    def y2(self):
4066        """y-coordinate of the end point."""
4067        return self.end.y

y-coordinate of the end point.

points
4069    @property
4070    def points(self):
4071        """Start and end"""
4072        return [self.start.point, self.end.point]

Start and end

vertices
4074    @property
4075    def vertices(self):
4076        """Start and end vertices."""
4077        return [self.start, self.end]

Start and end vertices.

array
4079    @property
4080    def array(self):
4081        """Homogeneous coordinates as a numpy array."""
4082        return array([self.start.array, self.end.array])

Homogeneous coordinates as a numpy array.

def rotate_point_3D( point: Sequence[float], line: Sequence[Sequence], angle: float) -> Sequence[float]:
4085def rotate_point_3D(point: Point, line: Line, angle: float) -> Point:
4086    """Rotate a 2d point (out of paper) about a 2d line by the given angle.
4087    This is used for animating mirror reflections.
4088     Args:
4089         point (Point): Point to rotate.
4090         line (Line): Line to rotate about.
4091         angle (float): Angle of rotation in radians.
4092
4093     Returns:
4094         Point: Rotated point.
4095    """
4096    from ..graphics.affine import rotation_matrix, translation_matrix
4097
4098    p1, p2 = line
4099    line_angle_ = line_angle(p1, p2)
4100    translation = translation_matrix(-p1[0], -p1[1])
4101    rotation = rotation_matrix(-line_angle_, (0, 0))
4102    xform = translation @ rotation
4103    x, y = point
4104    x, y, _ = [x, y, 1] @ xform
4105
4106    y *= cos(angle)
4107
4108    inv_translation = translation_matrix(p1[0], p1[1])
4109    inv_rotation = rotation_matrix(line_angle_, (0, 0))
4110    inv_xform = inv_rotation @ inv_translation
4111    x, y, _ = [x, y, 1] @ inv_xform
4112
4113    return (x, y)

Rotate a 2d point (out of paper) about a 2d line by the given angle. This is used for animating mirror reflections. Args: point (Point): Point to rotate. line (Line): Line to rotate about. angle (float): Angle of rotation in radians.

Returns: Point: Rotated point.

def rotate_line_3D( line: Sequence[Sequence], about: Sequence[Sequence], angle: float) -> Sequence[Sequence]:
4116def rotate_line_3D(line: Line, about: Line, angle: float) -> Line:
4117    """Rotate a 3d line about a 3d line by the given angle
4118
4119    Args:
4120        line (Line): Line to rotate.
4121        about (Line): Line to rotate about.
4122        angle (float): Angle of rotation in radians.
4123
4124    Returns:
4125        Line: Rotated line.
4126    """
4127    p1 = rotate_point_3D(line[0], about, angle)
4128    p2 = rotate_point_3D(line[1], about, angle)
4129
4130    return [p1, p2]

Rotate a 3d line about a 3d line by the given angle

Arguments:
  • line (Line): Line to rotate.
  • about (Line): Line to rotate about.
  • angle (float): Angle of rotation in radians.
Returns:

Line: Rotated line.