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]
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]])
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
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.
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.
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.
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.
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.
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.
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).
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).
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.
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, ...], ...}.
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.
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)]], ...}.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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)
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.
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.
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.
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.
4049 @property 4050 def x1(self): 4051 """x-coordinate of the start point.""" 4052 return self.start.x
x-coordinate of the start point.
4054 @property 4055 def y1(self): 4056 """y-coordinate of the start point.""" 4057 return self.start.y
y-coordinate of the start point.
4069 @property 4070 def points(self): 4071 """Start and end""" 4072 return [self.start.point, self.end.point]
Start and end
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.
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.