Spaces:
Runtime error
Runtime error
| # Copyright 2023 DeepMind Technologies Limited | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| # ============================================================================== | |
| """Numerical representation of geometry.""" | |
| from __future__ import annotations | |
| import math | |
| from typing import Any, Optional, Union | |
| import random | |
| import geometry as gm | |
| import matplotlib | |
| from matplotlib import pyplot as plt | |
| import matplotlib.colors as mcolors | |
| import numpy as np | |
| from numpy.random import uniform as unif # pylint: disable=g-importing-member | |
| import graph as gh | |
| from collections import defaultdict | |
| from itertools import combinations | |
| import numpy as np | |
| import matplotlib.patches | |
| import matplotlib.pyplot as plt | |
| from itertools import combinations | |
| matplotlib.use('Agg') | |
| ATOM = 1e-12 | |
| # Some variables are there for better code reading. | |
| # pylint: disable=unused-assignment | |
| # pylint: disable=unused-argument | |
| # pylint: disable=unused-variable | |
| # Naming in geometry is a little different | |
| # we stick to geometry naming to better read the code. | |
| # pylint: disable=invalid-name | |
| class Point: | |
| """Numerical point.""" | |
| def __init__(self, x, y): | |
| self.x = x | |
| self.y = y | |
| def __lt__(self, other: Point) -> bool: | |
| return (self.x, self.y) < (other.x, other.y) | |
| def __gt__(self, other: Point) -> bool: | |
| return (self.x, self.y) > (other.x, other.y) | |
| def __add__(self, p: Point) -> Point: | |
| return Point(self.x + p.x, self.y + p.y) | |
| def __sub__(self, p: Point) -> Point: | |
| return Point(self.x - p.x, self.y - p.y) | |
| def __mul__(self, f: float) -> Point: | |
| return Point(self.x * f, self.y * f) | |
| def __rmul__(self, f: float) -> Point: | |
| return self * f | |
| def __truediv__(self, f: float) -> Point: | |
| return Point(self.x / f, self.y / f) | |
| def __floordiv__(self, f: float) -> Point: | |
| div = self / f # true div | |
| return Point(int(div.x), int(div.y)) | |
| def __str__(self) -> str: | |
| return 'P({},{})'.format(self.x, self.y) | |
| def close(self, point: Point, tol: float = 1e-12) -> bool: | |
| return abs(self.x - point.x) < tol and abs(self.y - point.y) < tol | |
| def midpoint(self, p: Point) -> Point: | |
| return Point(0.5 * (self.x + p.x), 0.5 * (self.y + p.y)) | |
| def distance(self, p: Union[Point, Line, Circle]) -> float: | |
| if isinstance(p, Line): | |
| return p.distance(self) | |
| if isinstance(p, Circle): | |
| return abs(p.radius - self.distance(p.center)) | |
| dx = self.x - p.x | |
| dy = self.y - p.y | |
| return np.sqrt(dx * dx + dy * dy) | |
| def distance2(self, p: Point) -> float: | |
| if isinstance(p, Line): | |
| return p.distance(self) | |
| dx = self.x - p.x | |
| dy = self.y - p.y | |
| return dx * dx + dy * dy | |
| def rotatea(self, ang: float) -> Point: | |
| sinb, cosb = np.sin(ang), np.cos(ang) | |
| return self.rotate(sinb, cosb) | |
| def rotate(self, sinb: float, cosb: float) -> Point: | |
| x, y = self.x, self.y | |
| return Point(x * cosb - y * sinb, x * sinb + y * cosb) | |
| def flip(self) -> Point: | |
| return Point(-self.x, self.y) | |
| def perpendicular_line(self, line: Line) -> Line: | |
| return line.perpendicular_line(self) | |
| def foot(self, line: Line) -> Point: | |
| if isinstance(line, Line): | |
| l = line.perpendicular_line(self) | |
| return line_line_intersection(l, line) | |
| elif isinstance(line, Circle): | |
| c, r = line.center, line.radius | |
| return c + (self - c) * r / self.distance(c) | |
| raise ValueError('Dropping foot to weird type {}'.format(type(line))) | |
| def parallel_line(self, line: Line) -> Line: | |
| return line.parallel_line(self) | |
| def norm(self) -> float: | |
| return np.sqrt(self.x**2 + self.y**2) | |
| def cos(self, other: Point) -> float: | |
| x, y = self.x, self.y | |
| a, b = other.x, other.y | |
| return (x * a + y * b) / self.norm() / other.norm() | |
| def dot(self, other: Point) -> float: | |
| return self.x * other.x + self.y * other.y | |
| def sign(self, line: Line) -> int: | |
| return line.sign(self) | |
| def is_same(self, other: Point) -> bool: | |
| return self.distance(other) <= ATOM | |
| class Line: | |
| """Numerical line.""" | |
| def __init__( | |
| self, | |
| p1: Point = None, | |
| p2: Point = None, | |
| coefficients: tuple[int, int, int] = None, | |
| ): | |
| if p1 is None and p2 is None and coefficients is None: | |
| self.coefficients = None, None, None | |
| return | |
| a, b, c = coefficients or ( | |
| p1.y - p2.y, | |
| p2.x - p1.x, | |
| p1.x * p2.y - p2.x * p1.y, | |
| ) | |
| # Make sure a is always positive (or always negative for that matter) | |
| # With a == 0, Assuming a = +epsilon > 0 | |
| # Then b such that ax + by = 0 with y>0 should be negative. | |
| if a < 0.0 or a == 0.0 and b > 0.0: | |
| a, b, c = -a, -b, -c | |
| self.coefficients = a, b, c | |
| def parallel_line(self, p: Point) -> Line: | |
| a, b, _ = self.coefficients | |
| return Line(coefficients=(a, b, -a * p.x - b * p.y)) # pylint: disable=invalid-unary-operand-type | |
| def perpendicular_line(self, p: Point) -> Line: | |
| a, b, _ = self.coefficients | |
| return Line(p, p + Point(a, b)) | |
| def greater_than(self, other: Line) -> bool: | |
| a, b, _ = self.coefficients | |
| x, y, _ = other.coefficients | |
| # b/a > y/x | |
| return b * x > a * y | |
| def __gt__(self, other: Line) -> bool: | |
| return self.greater_than(other) | |
| def __lt__(self, other: Line) -> bool: | |
| return other.greater_than(self) | |
| def same(self, other: Line) -> bool: | |
| a, b, c = self.coefficients | |
| x, y, z = other.coefficients | |
| return close_enough(a * y, b * x) and close_enough(b * z, c * y) | |
| def equal(self, other: Line) -> bool: | |
| a, b, _ = self.coefficients | |
| x, y, _ = other.coefficients | |
| # b/a == y/x | |
| return b * x == a * y | |
| def less_than(self, other: Line) -> bool: | |
| a, b, _ = self.coefficients | |
| x, y, _ = other.coefficients | |
| # b/a > y/x | |
| return b * x < a * y | |
| def intersect(self, obj: Union[Line, Circle]) -> tuple[Point, ...]: | |
| if isinstance(obj, Line): | |
| return line_line_intersection(self, obj) | |
| if isinstance(obj, Circle): | |
| return line_circle_intersection(self, obj) | |
| def distance(self, p: Point) -> float: | |
| a, b, c = self.coefficients | |
| return abs(self(p.x, p.y)) / math.sqrt(a * a + b * b) | |
| def __call__(self, x: Point, y: Point = None) -> float: | |
| if isinstance(x, Point) and y is None: | |
| return self(x.x, x.y) | |
| a, b, c = self.coefficients | |
| return x * a + y * b + c | |
| def is_parallel(self, other: Line) -> bool: | |
| a, b, _ = self.coefficients | |
| x, y, _ = other.coefficients | |
| return abs(a * y - b * x) < ATOM | |
| def is_perp(self, other: Line) -> bool: | |
| a, b, _ = self.coefficients | |
| x, y, _ = other.coefficients | |
| return abs(a * x + b * y) < ATOM | |
| def cross(self, other: Line) -> float: | |
| a, b, _ = self.coefficients | |
| x, y, _ = other.coefficients | |
| return a * y - b * x | |
| def dot(self, other: Line) -> float: | |
| a, b, _ = self.coefficients | |
| x, y, _ = other.coefficients | |
| return a * x + b * y | |
| def point_at(self, x: float = None, y: float = None) -> Optional[Point]: | |
| """Get a point on line closest to (x, y).""" | |
| a, b, c = self.coefficients | |
| # ax + by + c = 0 | |
| if x is None and y is not None: | |
| if a != 0: | |
| return Point((-c - b * y) / a, y) # pylint: disable=invalid-unary-operand-type | |
| else: | |
| return None | |
| elif x is not None and y is None: | |
| if b != 0: | |
| return Point(x, (-c - a * x) / b) # pylint: disable=invalid-unary-operand-type | |
| else: | |
| return None | |
| elif x is not None and y is not None: | |
| if a * x + b * y + c == 0.0: | |
| return Point(x, y) | |
| return None | |
| def diff_side(self, p1: Point, p2: Point) -> Optional[bool]: | |
| d1 = self(p1.x, p1.y) | |
| d2 = self(p2.x, p2.y) | |
| if d1 == 0 or d2 == 0: | |
| return None | |
| return d1 * d2 < 0 | |
| def same_side(self, p1: Point, p2: Point) -> Optional[bool]: | |
| d1 = self(p1.x, p1.y) | |
| d2 = self(p2.x, p2.y) | |
| if d1 == 0 or d2 == 0: | |
| return None | |
| return d1 * d2 > 0 | |
| def sign(self, point: Point) -> int: | |
| s = self(point.x, point.y) | |
| if s > 0: | |
| return 1 | |
| elif s < 0: | |
| return -1 | |
| return 0 | |
| def is_same(self, other: Line) -> bool: | |
| a, b, c = self.coefficients | |
| x, y, z = other.coefficients | |
| return abs(a * y - b * x) <= ATOM and abs(b * z - c * y) <= ATOM | |
| def sample_within(self, points: list[Point], n: int = 5) -> list[Point]: | |
| """Sample a point within the boundary of points.""" | |
| center = sum(points, Point(0.0, 0.0)) * (1.0 / len(points)) | |
| radius = max([p.distance(center) for p in points]) | |
| if close_enough(center.distance(self), radius): | |
| center = center.foot(self) | |
| a, b = line_circle_intersection(self, Circle(center.foot(self), radius)) | |
| result = None | |
| best = -1.0 | |
| for _ in range(n): | |
| rand = unif(0.0, 1.0) | |
| x = a + (b - a) * rand | |
| mind = min([x.distance(p) for p in points]) | |
| if mind > best: | |
| best = mind | |
| result = x | |
| return [result] | |
| class InvalidLineIntersectError(Exception): | |
| pass | |
| class HalfLine(Line): | |
| """Numerical ray.""" | |
| def __init__(self, tail: Point, head: Point): # pylint: disable=super-init-not-called | |
| self.line = Line(tail, head) | |
| self.coefficients = self.line.coefficients | |
| self.tail = tail | |
| self.head = head | |
| def intersect(self, obj: Union[Line, HalfLine, Circle, HoleCircle]) -> Point: | |
| if isinstance(obj, (HalfLine, Line)): | |
| return line_line_intersection(self.line, obj) | |
| exclude = [self.tail] | |
| if isinstance(obj, HoleCircle): | |
| exclude += [obj.hole] | |
| a, b = line_circle_intersection(self.line, obj) | |
| if any([a.close(x) for x in exclude]): | |
| return b | |
| if any([b.close(x) for x in exclude]): | |
| return a | |
| v = self.head - self.tail | |
| va = a - self.tail | |
| vb = b - self.tail | |
| if v.dot(va) > 0: | |
| return a | |
| if v.dot(vb) > 0: | |
| return b | |
| raise InvalidLineIntersectError() | |
| def sample_within(self, points: list[Point], n: int = 5) -> list[Point]: | |
| center = sum(points, Point(0.0, 0.0)) * (1.0 / len(points)) | |
| radius = max([p.distance(center) for p in points]) | |
| if close_enough(center.distance(self.line), radius): | |
| center = center.foot(self) | |
| a, b = line_circle_intersection(self, Circle(center.foot(self), radius)) | |
| if (a - self.tail).dot(self.head - self.tail) > 0: | |
| a, b = self.tail, a | |
| else: | |
| a, b = self.tail, b # pylint: disable=self-assigning-variable | |
| result = None | |
| best = -1.0 | |
| for _ in range(n): | |
| x = a + (b - a) * unif(0.0, 1.0) | |
| mind = min([x.distance(p) for p in points]) | |
| if mind > best: | |
| best = mind | |
| result = x | |
| return [result] | |
| def _perpendicular_bisector(p1: Point, p2: Point) -> Line: | |
| midpoint = (p1 + p2) * 0.5 | |
| return Line(midpoint, midpoint + Point(p2.y - p1.y, p1.x - p2.x)) | |
| def same_sign( | |
| a: Point, b: Point, c: Point, d: Point, e: Point, f: Point | |
| ) -> bool: | |
| a, b, c, d, e, f = map(lambda p: p.sym, [a, b, c, d, e, f]) | |
| ab, cb = a - b, c - b | |
| de, fe = d - e, f - e | |
| return (ab.x * cb.y - ab.y * cb.x) * (de.x * fe.y - de.y * fe.x) > 0 | |
| class Circle: | |
| """Numerical circle.""" | |
| def __init__( | |
| self, | |
| center: Optional[Point] = None, | |
| radius: Optional[float] = None, | |
| p1: Optional[Point] = None, | |
| p2: Optional[Point] = None, | |
| p3: Optional[Point] = None, | |
| ): | |
| if not center: | |
| if not (p1 and p2 and p3): | |
| self.center = self.radius = self.r2 = None | |
| return | |
| # raise ValueError('Circle without center need p1 p2 p3') | |
| l12 = _perpendicular_bisector(p1, p2) | |
| l23 = _perpendicular_bisector(p2, p3) | |
| center = line_line_intersection(l12, l23) | |
| self.center = center | |
| self.a, self.b = center.x, center.y | |
| if not radius: | |
| if not (p1 or p2 or p3): | |
| raise ValueError('Circle needs radius or p1 or p2 or p3') | |
| p = p1 or p2 or p3 | |
| self.r2 = (self.a - p.x) ** 2 + (self.b - p.y) ** 2 | |
| self.radius = math.sqrt(self.r2) | |
| else: | |
| self.radius = radius | |
| self.r2 = radius * radius | |
| def intersect(self, obj: Union[Line, Circle]) -> tuple[Point, ...]: | |
| if isinstance(obj, Line): | |
| return obj.intersect(self) | |
| if isinstance(obj, Circle): | |
| return circle_circle_intersection(self, obj) | |
| def sample_within(self, points: list[Point], n: int = 5) -> list[Point]: | |
| """Sample a point within the boundary of points.""" | |
| result = None | |
| best = -1.0 | |
| for _ in range(n): | |
| ang = unif(0.0, 2.0) * np.pi | |
| x = self.center + Point(np.cos(ang), np.sin(ang)) * self.radius | |
| mind = min([x.distance(p) for p in points]) | |
| if mind > best: | |
| best = mind | |
| result = x | |
| return [result] | |
| class SemiCircle(Circle): | |
| """Numerical semicircle, inherits from Circle.""" | |
| def __init__( | |
| self, | |
| center: Optional[Point] = None, | |
| radius: Optional[float] = None, | |
| p1: Optional[Point] = None, | |
| p2: Optional[Point] = None, | |
| p3: Optional[Point] = None, | |
| ): | |
| self.p1 = p1 | |
| self.p2 = p2 | |
| self.p3 = p3 | |
| # Initialize as a Circle | |
| super().__init__(center, radius, p1, p2, p3) | |
| # If p1 and p2 define a diameter, set the center and radius accordingly | |
| if p1 and p2 and not center: | |
| self.center = Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2) | |
| self.radius = p1.distance(p2) / 2 | |
| self.r2 = self.radius ** 2 | |
| # Define the direction or plane for the semicircle (important for sampling and boundaries) | |
| def is_within_boundary(self, point: Point) -> bool: | |
| """Check if a point is within the boundary of the semicircle.""" | |
| vector_to_point = point - self.center | |
| angle = math.atan2(vector_to_point.y, vector_to_point.x) | |
| # Normalize the angle within [0, 2*pi] | |
| angle = angle if angle >= 0 else (2 * np.pi + angle) | |
| # Check if the point is within the semicircle (half of the circle) | |
| return -np.pi / 2 <= angle <= np.pi / 2 | |
| def sample_within(self, points: list[Point], n: int = 5) -> list[Point]: | |
| """Sample a point within the semicircle.""" | |
| result = None | |
| best = -1.0 | |
| for _ in range(n): | |
| # Generate a random angle between -π/2 and π/2 for the semicircle | |
| ang = unif(-0.5, 0.5) * np.pi | |
| x = self.center + Point(np.cos(ang), np.sin(ang)) * self.radius | |
| # Check if the sampled point is within the active part of the semicircle | |
| if not self.is_within_boundary(x): | |
| continue | |
| # Find the minimum distance between the generated point and the provided points | |
| mind = min([x.distance(p) for p in points]) | |
| if mind > best: | |
| best = mind | |
| result = x | |
| return [result] | |
| def intersect(self, obj: Union[Line, Circle]) -> tuple[Point, ...]: | |
| """Find intersection points with a Line or another Circle, constrained to the semicircle.""" | |
| if isinstance(obj, Line): | |
| intersections = obj.intersect(self) | |
| elif isinstance(obj, Circle): | |
| intersections = circle_circle_intersection(self, obj) | |
| else: | |
| return tuple() | |
| # Filter intersections to only return points within the semicircle | |
| return tuple(p for p in intersections if self.is_within_boundary(p)) | |
| class HoleCircle(Circle): | |
| """Numerical circle with a missing point.""" | |
| def __init__(self, center: Point, radius: float, hole: Point): | |
| super().__init__(center, radius) | |
| self.hole = hole | |
| def intersect(self, obj: Union[Line, HalfLine, Circle, HoleCircle]) -> Point: | |
| if isinstance(obj, Line): | |
| a, b = line_circle_intersection(obj, self) | |
| if a.close(self.hole): | |
| return b | |
| return a | |
| if isinstance(obj, HalfLine): | |
| return obj.intersect(self) | |
| if isinstance(obj, Circle): | |
| a, b = circle_circle_intersection(obj, self) | |
| if a.close(self.hole): | |
| return b | |
| return a | |
| if isinstance(obj, HoleCircle): | |
| a, b = circle_circle_intersection(obj, self) | |
| if a.close(self.hole) or a.close(obj.hole): | |
| return b | |
| return a | |
| def solve_quad(a: float, b: float, c: float) -> tuple[float, float]: | |
| """Solve a x^2 + bx + c = 0.""" | |
| a = 2 * a | |
| d = b * b - 2 * a * c | |
| if d < 0: | |
| return None # the caller should expect this result. | |
| y = math.sqrt(d) | |
| return (-b - y) / a, (-b + y) / a | |
| def circle_circle_intersection(c1: Circle, c2: Circle) -> tuple[Point, Point]: | |
| """Returns a pair of Points as intersections of c1 and c2.""" | |
| # circle 1: (x0, y0), radius r0 | |
| # circle 2: (x1, y1), radius r1 | |
| x0, y0, r0 = c1.a, c1.b, c1.radius | |
| x1, y1, r1 = c2.a, c2.b, c2.radius | |
| d = math.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2) | |
| if d == 0: | |
| raise InvalidQuadSolveError() | |
| a = (r0**2 - r1**2 + d**2) / (2 * d) | |
| h = r0**2 - a**2 | |
| if h < 0: | |
| raise InvalidQuadSolveError() | |
| h = np.sqrt(h) | |
| x2 = x0 + a * (x1 - x0) / d | |
| y2 = y0 + a * (y1 - y0) / d | |
| x3 = x2 + h * (y1 - y0) / d | |
| y3 = y2 - h * (x1 - x0) / d | |
| x4 = x2 - h * (y1 - y0) / d | |
| y4 = y2 + h * (x1 - x0) / d | |
| return Point(x3, y3), Point(x4, y4) | |
| class InvalidQuadSolveError(Exception): | |
| pass | |
| def line_circle_intersection(line: Line, circle: Circle) -> tuple[Point, Point]: | |
| """Returns a pair of points as intersections of line and circle.""" | |
| a, b, c = line.coefficients | |
| r = float(circle.radius) | |
| center = circle.center | |
| p, q = center.x, center.y | |
| if b == 0: | |
| x = -c / a | |
| x_p = x - p | |
| x_p2 = x_p * x_p | |
| y = solve_quad(1, -2 * q, q * q + x_p2 - r * r) | |
| if y is None: | |
| raise InvalidQuadSolveError() | |
| y1, y2 = y | |
| return (Point(x, y1), Point(x, y2)) | |
| if a == 0: | |
| y = -c / b | |
| y_q = y - q | |
| y_q2 = y_q * y_q | |
| x = solve_quad(1, -2 * p, p * p + y_q2 - r * r) | |
| if x is None: | |
| raise InvalidQuadSolveError() | |
| x1, x2 = x | |
| return (Point(x1, y), Point(x2, y)) | |
| c_ap = c + a * p | |
| a2 = a * a | |
| y = solve_quad( | |
| a2 + b * b, 2 * (b * c_ap - a2 * q), c_ap * c_ap + a2 * (q * q - r * r) | |
| ) | |
| if y is None: | |
| raise InvalidQuadSolveError() | |
| y1, y2 = y | |
| return Point(-(b * y1 + c) / a, y1), Point(-(b * y2 + c) / a, y2) | |
| def _check_between(a: Point, b: Point, c: Point) -> bool: | |
| """Whether a is between b & c.""" | |
| return (a - b).dot(c - b) > 0 and (a - c).dot(b - c) > 0 | |
| def circle_segment_intersect( | |
| circle: Circle, p1: Point, p2: Point | |
| ) -> list[Point]: | |
| l = Line(p1, p2) | |
| px, py = line_circle_intersection(l, circle) | |
| result = [] | |
| if _check_between(px, p1, p2): | |
| result.append(px) | |
| if _check_between(py, p1, p2): | |
| result.append(py) | |
| return result | |
| def semicircle_segment_intersect( | |
| circle: SemiCircle, p1: Point, p2: Point | |
| ) -> list[Point]: | |
| l = Line(p1, p2) | |
| px, py = line_circle_intersection(l, circle) | |
| result = [] | |
| if _check_between(px, p1, p2): | |
| result.append(px) | |
| if _check_between(py, p1, p2): | |
| result.append(py) | |
| return result | |
| def line_segment_intersection(l: Line, A: Point, B: Point) -> Point: # pylint: disable=invalid-name | |
| a, b, c = l.coefficients | |
| x1, y1, x2, y2 = A.x, A.y, B.x, B.y | |
| dx, dy = x2 - x1, y2 - y1 | |
| alpha = (-c - a * x1 - b * y1) / (a * dx + b * dy) | |
| return Point(x1 + alpha * dx, y1 + alpha * dy) | |
| def line_line_intersection(l1: Line, l2: Line) -> Point: | |
| a1, b1, c1 = l1.coefficients | |
| a2, b2, c2 = l2.coefficients | |
| # a1x + b1y + c1 = 0 | |
| # a2x + b2y + c2 = 0 | |
| d = a1 * b2 - a2 * b1 | |
| if d == 0: | |
| raise InvalidLineIntersectError | |
| return Point((c2 * b1 - c1 * b2) / d, (c1 * a2 - c2 * a1) / d) | |
| def check_too_close( | |
| newpoints: list[Point], points: list[Point], tol: int = 0.1 | |
| ) -> bool: | |
| if not points: | |
| return False | |
| avg = sum(points, Point(0.0, 0.0)) * 1.0 / len(points) | |
| mindist = min([p.distance(avg) for p in points]) | |
| for p0 in newpoints: | |
| for p1 in points: | |
| if p0.distance(p1) < tol * mindist: | |
| return True | |
| return False | |
| def check_too_far( | |
| newpoints: list[Point], points: list[Point], tol: int = 4 | |
| ) -> bool: | |
| if len(points) < 2: | |
| return False | |
| avg = sum(points, Point(0.0, 0.0)) * 1.0 / len(points) | |
| maxdist = max([p.distance(avg) for p in points]) | |
| for p in newpoints: | |
| if p.distance(avg) > maxdist * tol: | |
| return True | |
| return False | |
| def check_aconst(args: list[Point]) -> bool: | |
| a, b, c, d, num, den = args | |
| d = d + a - c | |
| ang = ang_between(a, b, d) | |
| if ang < 0: | |
| ang += np.pi | |
| return close_enough(ang, num * np.pi / den) | |
| def check(name: str, args: list[Union[gm.Point, Point]]) -> bool: | |
| """Numerical check.""" | |
| if name == 'eqangle6': | |
| name = 'eqangle' | |
| elif name == 'eqratio6': | |
| name = 'eqratio' | |
| elif name in ['simtri2', 'simtri*']: | |
| name = 'simtri' | |
| elif name in ['contri2', 'contri*']: | |
| name = 'contri' | |
| elif name == 'para': | |
| name = 'para_or_coll' | |
| elif name == 'on_line': | |
| name = 'coll' | |
| elif name in ['rcompute', 'acompute']: | |
| return True | |
| elif name in ['fixl', 'fixc', 'fixb', 'fixt', 'fixp']: | |
| return True | |
| fn_name = 'check_' + name | |
| if fn_name not in globals(): | |
| return None | |
| fun = globals()['check_' + name] | |
| args = [p.num if isinstance(p, gm.Point) else p for p in args] | |
| return fun(args) | |
| def check_circle(points: list[Point]) -> bool: | |
| if len(points) != 4: | |
| return False | |
| o, a, b, c = points | |
| oa, ob, oc = o.distance(a), o.distance(b), o.distance(c) | |
| return close_enough(oa, ob) and close_enough(ob, oc) | |
| def check_semicircle(points: list[Point]) -> bool: | |
| if len(points) != 4: | |
| return False | |
| o, a, b, c = points | |
| oa, ob, oc = o.distance(a), o.distance(b), o.distance(c) | |
| return close_enough(oa, ob) and close_enough(ob, oc) | |
| def check_coll(points: list[Point]) -> bool: | |
| a, b = points[:2] | |
| l = Line(a, b) | |
| for p in points[2:]: | |
| if abs(l(p.x, p.y)) > ATOM: | |
| return False | |
| return True | |
| def check_ncoll(points: list[Point]) -> bool: | |
| return not check_coll(points) | |
| def check_sameside(points: list[Point]) -> bool: | |
| b, a, c, y, x, z = points | |
| # whether b is to the same side of a & c as y is to x & z | |
| ba = b - a | |
| bc = b - c | |
| yx = y - x | |
| yz = y - z | |
| return ba.dot(bc) * yx.dot(yz) > 0 | |
| def check_para_or_coll(points: list[Point]) -> bool: | |
| return check_para(points) or check_coll(points) | |
| def check_para(points: list[Point]) -> bool: | |
| a, b, c, d = points | |
| ab = Line(a, b) | |
| cd = Line(c, d) | |
| if ab.same(cd): | |
| return False | |
| return ab.is_parallel(cd) | |
| def check_perp(points: list[Point]) -> bool: | |
| a, b, c, d = points | |
| ab = Line(a, b) | |
| cd = Line(c, d) | |
| return ab.is_perp(cd) | |
| def check_cyclic(points: list[Point]) -> bool: | |
| points = list(set(points)) | |
| (a, b, c, *ps) = points | |
| circle = Circle(p1=a, p2=b, p3=c) | |
| for d in ps: | |
| if not close_enough(d.distance(circle.center), circle.radius): | |
| return False | |
| return True | |
| def bring_together( | |
| a: Point, b: Point, c: Point, d: Point | |
| ) -> tuple[Point, Point, Point, Point]: | |
| ab = Line(a, b) | |
| cd = Line(c, d) | |
| x = line_line_intersection(ab, cd) | |
| unit = Circle(center=x, radius=1.0) | |
| y, _ = line_circle_intersection(ab, unit) | |
| z, _ = line_circle_intersection(cd, unit) | |
| return x, y, x, z | |
| def same_clock( | |
| a: Point, b: Point, c: Point, d: Point, e: Point, f: Point | |
| ) -> bool: | |
| ba = b - a | |
| cb = c - b | |
| ed = e - d | |
| fe = f - e | |
| return (ba.x * cb.y - ba.y * cb.x) * (ed.x * fe.y - ed.y * fe.x) > 0 | |
| def check_const_angle(points: list[Point]) -> bool: | |
| """Check if the angle is equal to the given constant.""" | |
| a, b, c, d, m, n = points | |
| a, b, c, d = bring_together(a, b, c, d) | |
| ba = b - a | |
| dc = d - c | |
| a3 = np.arctan2(ba.y, ba.x) | |
| a4 = np.arctan2(dc.y, dc.x) | |
| y = a3 - a4 | |
| return close_enough(m / n % 1, y / np.pi % 1) | |
| def check_eqangle(points: list[Point]) -> bool: | |
| """Check if 8 points make 2 equal angles.""" | |
| a, b, c, d, e, f, g, h = points | |
| ab = Line(a, b) | |
| cd = Line(c, d) | |
| ef = Line(e, f) | |
| gh = Line(g, h) | |
| if ab.is_parallel(cd): | |
| return ef.is_parallel(gh) | |
| if ef.is_parallel(gh): | |
| return ab.is_parallel(cd) | |
| a, b, c, d = bring_together(a, b, c, d) | |
| e, f, g, h = bring_together(e, f, g, h) | |
| ba = b - a | |
| dc = d - c | |
| fe = f - e | |
| hg = h - g | |
| sameclock = (ba.x * dc.y - ba.y * dc.x) * (fe.x * hg.y - fe.y * hg.x) > 0 | |
| if not sameclock: | |
| ba = ba * -1.0 | |
| a1 = np.arctan2(fe.y, fe.x) | |
| a2 = np.arctan2(hg.y, hg.x) | |
| x = a1 - a2 | |
| a3 = np.arctan2(ba.y, ba.x) | |
| a4 = np.arctan2(dc.y, dc.x) | |
| y = a3 - a4 | |
| xy = (x - y) % (2 * np.pi) | |
| return close_enough(xy, 0, tol=1e-11) or close_enough( | |
| xy, 2 * np.pi, tol=1e-11 | |
| ) | |
| def check_eqratio(points: list[Point]) -> bool: | |
| a, b, c, d, e, f, g, h = points | |
| ab = a.distance(b) | |
| cd = c.distance(d) | |
| ef = e.distance(f) | |
| gh = g.distance(h) | |
| return close_enough(ab * gh, cd * ef) | |
| def check_cong(points: list[Point]) -> bool: | |
| a, b, c, d = points | |
| return close_enough(a.distance(b), c.distance(d)) | |
| def check_midp(points: list[Point]) -> bool: | |
| a, b, c = points | |
| return check_coll(points) and close_enough(a.distance(b), a.distance(c)) | |
| def check_simtri(points: list[Point]) -> bool: | |
| """Check if 6 points make a pair of similar triangles.""" | |
| a, b, c, x, y, z = points | |
| ab = a.distance(b) | |
| bc = b.distance(c) | |
| ca = c.distance(a) | |
| xy = x.distance(y) | |
| yz = y.distance(z) | |
| zx = z.distance(x) | |
| tol = 1e-9 | |
| return close_enough(ab * yz, bc * xy, tol) and close_enough( | |
| bc * zx, ca * yz, tol | |
| ) | |
| def check_contri(points: list[Point]) -> bool: | |
| a, b, c, x, y, z = points | |
| ab = a.distance(b) | |
| bc = b.distance(c) | |
| ca = c.distance(a) | |
| xy = x.distance(y) | |
| yz = y.distance(z) | |
| zx = z.distance(x) | |
| tol = 1e-9 | |
| return ( | |
| close_enough(ab, xy, tol) | |
| and close_enough(bc, yz, tol) | |
| and close_enough(ca, zx, tol) | |
| ) | |
| def check_ratio(points: list[Point]) -> bool: | |
| a, b, c, d, m, n = points | |
| ab = a.distance(b) | |
| cd = c.distance(d) | |
| return close_enough(ab * n, cd * m) | |
| def draw_angle( | |
| ax: matplotlib.axes.Axes, | |
| head: Point, | |
| p1: Point, | |
| p2: Point, | |
| color: Any = 'red', | |
| alpha: float = 0.5, | |
| frac: float = 1.0, | |
| ) -> None: | |
| """Draw an angle on plt ax.""" | |
| d1 = p1 - head | |
| d2 = p2 - head | |
| a1 = np.arctan2(float(d1.y), float(d1.x)) | |
| a2 = np.arctan2(float(d2.y), float(d2.x)) | |
| a1, a2 = a1 * 180 / np.pi, a2 * 180 / np.pi | |
| a1, a2 = a1 % 360, a2 % 360 | |
| if a1 > a2: | |
| a1, a2 = a2, a1 | |
| if a2 - a1 > 180: | |
| a1, a2 = a2, a1 | |
| b1, b2 = a1, a2 | |
| if b1 > b2: | |
| b2 += 360 | |
| d = b2 - b1 | |
| # if d >= 90: | |
| # return | |
| scale = min(2.0, 90 / d) | |
| scale = max(scale, 0.4) | |
| fov = matplotlib.patches.Wedge( | |
| (float(head.x), float(head.y)), | |
| unif(0.075, 0.125) * scale * frac, | |
| a1, | |
| a2, | |
| color=color, | |
| alpha=alpha, | |
| ) | |
| ax.add_artist(fov) | |
| def naming_position( | |
| ax: matplotlib.axes.Axes, p: Point, lines: list[Line], circles: list[Circle], semicircles: list[SemiCircle] | |
| ) -> tuple[float, float]: | |
| """Figure out a good naming position on the drawing.""" | |
| _ = ax | |
| r = 0.08 | |
| c = Circle(center=p, radius=r) | |
| sc = SemiCircle(center=p, radius=r) | |
| avoid = [] | |
| for p1, p2 in lines: | |
| try: | |
| avoid.extend(circle_segment_intersect(c, p1, p2)) | |
| avoid.extend(semicircle_segment_intersect(sc, p1, p2)) | |
| except InvalidQuadSolveError: | |
| continue | |
| for x in circles: | |
| try: | |
| avoid.extend(circle_circle_intersection(c, x)) | |
| except InvalidQuadSolveError: | |
| continue | |
| if not avoid: | |
| return [p.x + 0.01, p.y + 0.01] | |
| angs = sorted([ang_of(p, a) for a in avoid]) | |
| angs += [angs[0] + 2 * np.pi] | |
| angs = [(angs[i + 1] - a, a) for i, a in enumerate(angs[:-1])] | |
| d, a = max(angs) | |
| ang = a + d / 2 | |
| name_pos = p + Point(np.cos(ang), np.sin(ang)) * r | |
| x, y = (name_pos.x - r / 1.5, name_pos.y - r / 1.5) | |
| return x, y | |
| def draw_point( | |
| ax: matplotlib.axes.Axes, | |
| p: Point, | |
| name: str, | |
| lines: list[Line], | |
| circles: list[Circle], | |
| semicircles: list[SemiCircle], | |
| color: Any = 'white', | |
| size: float = 15, | |
| ) -> None: | |
| """draw a point.""" | |
| ax.scatter(p.x, p.y, color=color, s=size) | |
| if color == 'white': | |
| color = 'lightgreen' | |
| else: | |
| color = 'grey' | |
| name = name.upper() | |
| if len(name) > 1: | |
| name = name[0] + '_' + name[1:] | |
| ax.annotate( | |
| name, naming_position(ax, p, lines, circles, semicircles), color=color, fontsize=15 | |
| ) | |
| def _draw_line( | |
| ax: matplotlib.axes.Axes, | |
| p1: Point, | |
| p2: Point, | |
| color: Any = 'white', | |
| lw: float = 1.2, | |
| alpha: float = 0.8, | |
| ) -> None: | |
| """Draw a line in matplotlib.""" | |
| ls = '-' | |
| if color == '--': | |
| color = 'black' | |
| ls = '--' | |
| lx, ly = (p1.x, p2.x), (p1.y, p2.y) | |
| ax.plot(lx, ly, color=color, lw=lw, alpha=alpha, ls=ls) | |
| def draw_line( | |
| ax: matplotlib.axes.Axes, line: Line, color: Any = 'white', draw: bool = True | |
| ) -> tuple[Point, Point]: | |
| """Draw a line.""" | |
| points = line.neighbors(gm.Point) | |
| if len(points) <= 1: | |
| return | |
| points = [p.num for p in points] | |
| p1, p2 = points[:2] | |
| pmin, pmax = (p1, 0.0), (p2, (p2 - p1).dot(p2 - p1)) | |
| for p in points[2:]: | |
| v = (p - p1).dot(p2 - p1) | |
| if v < pmin[1]: | |
| pmin = p, v | |
| if v > pmax[1]: | |
| pmax = p, v | |
| p1, p2 = pmin[0], pmax[0] | |
| if draw: | |
| _draw_line(ax, p1, p2, color=color) | |
| return p1, p2 | |
| else: | |
| return p1, p2 | |
| def _draw_circle( | |
| ax: matplotlib.axes.Axes, c: Circle, color: Any = 'cyan', lw: float = 1.2 | |
| ) -> None: | |
| ls = '-' | |
| if color == '--': | |
| color = 'black' | |
| ls = '--' | |
| ax.add_patch( | |
| plt.Circle( | |
| (c.center.x, c.center.y), | |
| c.radius, | |
| color=color, | |
| alpha=0.8, | |
| fill=False, | |
| lw=lw, | |
| ls=ls, | |
| ) | |
| ) | |
| def draw_circle( | |
| ax: matplotlib.axes.Axes, circle: Circle, color: Any = 'cyan' | |
| ) -> Circle: | |
| """Draw a circle.""" | |
| name_circle = circle.name | |
| if circle.num is not None: | |
| circle = circle.num | |
| else: | |
| points = circle.neighbors(gm.Point) | |
| if len(points) <= 2: | |
| return | |
| points = [p.num for p in points] | |
| print(len(points)) | |
| p1, p2, p3 = points[:3] | |
| circle = Circle(p1=p1, p2=p2, p3=p3) | |
| if "," in name_circle: | |
| _draw_circle(ax, circle, color) | |
| return circle | |
| else: | |
| return circle | |
| def check_points_semicircle(p1, p2, p3): | |
| """ | |
| Check if three points are in a semicircle, and determine the circle center, radius, | |
| and points forming the diameter if applicable. If no pair forms a diameter, calculate | |
| the circle center passing through all three points. | |
| Parameters: | |
| p1, p2, p3 (tuple): Three points as (x, y) coordinates. | |
| Returns: | |
| dict: A dictionary containing: | |
| - 'center': (cx, cy), the circle center. | |
| - 'radius': The radius of the circle. | |
| - 'diameter_points': A tuple of two points that form the diameter (or None). | |
| - 'is_valid': True if a circle can be formed; False otherwise. | |
| """ | |
| # Unpack points | |
| x1, y1 = p1 | |
| x2, y2 = p2 | |
| x3, y3 = p3 | |
| # Calculate circumcenter | |
| A = np.array([[x1 - x2, y1 - y2], [x1 - x3, y1 - y3]]) | |
| B = np.array([((x1**2 - x2**2) + (y1**2 - y2**2)) / 2, ((x1**2 - x3**2) + (y1**2 - y3**2)) / 2]) | |
| try: | |
| center = np.linalg.solve(A, B) # Solving linear system to get circle center | |
| except np.linalg.LinAlgError: | |
| return {'is_valid': False} # Points are collinear, no unique circle | |
| cx, cy = center | |
| radius = np.sqrt((x1 - cx)**2 + (y1 - cy)**2) | |
| # Function to check if two points form a diameter | |
| def is_diameter(px, py, qx, qy): | |
| midpoint_x, midpoint_y = (px + qx) / 2, (py + qy) / 2 | |
| return np.isclose(midpoint_x, cx) and np.isclose(midpoint_y, cy) | |
| # Check for diameter | |
| if is_diameter(x1, y1, x2, y2): | |
| diameter_points = (p1, p2) | |
| elif is_diameter(x1, y1, x3, y3): | |
| diameter_points = (p1, p3) | |
| elif is_diameter(x2, y2, x3, y3): | |
| diameter_points = (p2, p3) | |
| else: | |
| diameter_points = None # No pair forms a diameter; use circumcenter | |
| return { | |
| 'center': center, | |
| 'radius': radius, | |
| 'diameter_points': diameter_points, | |
| 'is_valid': True | |
| } | |
| def _draw_semicircle( | |
| ax: matplotlib.axes.Axes, P1: Point = None, P2: Point = None, P3: Point = None, color: Any = 'cyan', lw: float = 1.2 | |
| ) -> None: | |
| """ | |
| Draws a semicircle passing through three points or with one or two points on the diameter. | |
| Parameters: | |
| ax (matplotlib.axes.Axes): The Matplotlib Axes on which the semicircle will be drawn. | |
| P1, P2, P3 (Point): The three points through which the semicircle will pass. | |
| color (Any): Color of the semicircle. | |
| lw (float): Line width of the semicircle. | |
| """ | |
| points = [P for P in (P1, P2, P3) if P is not None] # Filter out None values | |
| if len(points) == 2: | |
| cx = (P1.x + P2.x) / 2 | |
| cy = (P1.y + P2.y) / 2 | |
| radius = np.sqrt((P2.x - P1.x) ** 2 + (P2.y - P1.y) ** 2) / 2 | |
| # Compute angle of the diameter | |
| angle_diameter = np.arctan2(P2.y - P1.y, P2.x - P1.x) | |
| # Randomly determine the orientation of the semicircle | |
| offset_angle = np.pi / 2 if random.choice([True, False]) else -np.pi / 2 | |
| # Start and end angles for the semicircle | |
| start_angle = angle_diameter + offset_angle | |
| end_angle = start_angle + np.pi | |
| # Generate points for the semicircle | |
| t = np.linspace(start_angle, end_angle, 100) | |
| x = cx + radius * np.cos(t) | |
| y = cy + radius * np.sin(t) | |
| # Plot the semicircle | |
| ax.plot(x, y, color=color, lw=lw) | |
| if len(points) == 3: | |
| result = check_points_semicircle((P1.x, P1.y), (P2.x, P2.y), (P3.x, P3.y)) | |
| if not result['is_valid']: | |
| print("Points are collinear; cannot form a semicircle.") | |
| return | |
| cx, cy = result['center'] | |
| radius = result['radius'] | |
| diameter_points = result['diameter_points'] | |
| # If no pair forms a diameter, determine angles for all three points | |
| if diameter_points is None: | |
| # Calculate angles of all three points relative to the circle's center | |
| angles = np.arctan2( | |
| [P1.y - cy, P2.y - cy, P3.y - cy], | |
| [P1.x - cx, P2.x - cx, P3.x - cx] | |
| ) | |
| angles = (angles + 2 * np.pi) % (2 * np.pi) # Normalize to [0, 2π] | |
| # Determine the start and end angle for the semicircle | |
| start_angle = np.min(angles) | |
| end_angle = np.max(angles) | |
| if end_angle - start_angle > np.pi: | |
| start_angle, end_angle = end_angle, start_angle + 2 * np.pi | |
| else: | |
| # Use diameter points to define the semicircle angles | |
| px, py = diameter_points[0] | |
| qx, qy = diameter_points[1] | |
| start_angle = np.arctan2(py - cy, px - cx) | |
| end_angle = np.arctan2(qy - cy, qx - cx) | |
| if end_angle - start_angle > np.pi: | |
| start_angle, end_angle = end_angle, start_angle + 2 * np.pi | |
| # Generate points for the semicircle | |
| t = np.linspace(start_angle, end_angle, 100) | |
| x = cx + radius * np.cos(t) | |
| y = cy + radius * np.sin(t) | |
| # Plot the semicircle | |
| ax.plot(x, y, color=color, lw=lw) | |
| def draw_semicircle( | |
| ax: matplotlib.axes.Axes, semicircle: SemiCircle, color: Any = 'cyan' | |
| ) -> SemiCircle: | |
| """Draw a semicircle.""" | |
| if semicircle.num is not None: | |
| semicircle = semicircle.num | |
| else: | |
| points = semicircle.neighbors(gm.Point) | |
| if len(points) <= 2: | |
| return | |
| points = [p.num for p in points] | |
| p1, p2, p3 = points[:3] | |
| semicircle = SemiCircle(p1=p1, p2=p2, p3=p3) | |
| print(semicircle.p1, semicircle.p2, semicircle.p3) | |
| _draw_semicircle(ax, semicircle.p1, semicircle.p2, semicircle.p3, color=color) | |
| _draw_line(ax, semicircle.p1, semicircle.p2) | |
| _draw_line(ax, semicircle.p2, semicircle.p3) | |
| _draw_line(ax, semicircle.p1, semicircle.p3) | |
| return semicircle | |
| def mark_segment( | |
| ax: matplotlib.axes.Axes, p1: Point, p2: Point, color: Any, alpha: float | |
| ) -> None: | |
| _ = alpha | |
| x, y = (p1.x + p2.x) / 2, (p1.y + p2.y) / 2 | |
| ax.scatter(x, y, color=color, alpha=1.0, marker='o', s=50) | |
| def highlight_angle( | |
| ax: matplotlib.axes.Axes, | |
| a: Point, | |
| b: Point, | |
| c: Point, | |
| d: Point, | |
| color: Any, | |
| alpha: float, | |
| ) -> None: | |
| """Highlight an angle between ab and cd with (color, alpha).""" | |
| try: | |
| a, b, c, d = bring_together(a, b, c, d) | |
| except: # pylint: disable=bare-except | |
| return | |
| draw_angle(ax, a, b, d, color=color, alpha=alpha, frac=1.0) | |
| def highlight( | |
| ax: matplotlib.axes.Axes, | |
| name: str, | |
| args: list[gm.Point], | |
| lcolor: Any, | |
| color1: Any, | |
| color2: Any, | |
| ) -> None: | |
| """Draw highlights.""" | |
| args = list(map(lambda x: x.num if isinstance(x, gm.Point) else x, args)) | |
| if name == 'coll': | |
| a, b, c = args | |
| a, b = max(a, b, c), min(a, b, c) | |
| _draw_line(ax, a, b, color=color1, lw=2.0) | |
| if name == 'para': | |
| a, b, c, d = args | |
| _draw_line(ax, a, b, color=color1, lw=2.0) | |
| _draw_line(ax, c, d, color=color2, lw=2.0) | |
| if name == 'eqangle': | |
| a, b, c, d, e, f, g, h = args | |
| x = line_line_intersection(Line(a, b), Line(c, d)) | |
| if b.distance(x) > a.distance(x): | |
| a, b = b, a | |
| if d.distance(x) > c.distance(x): | |
| c, d = d, c | |
| a, b, d = x, a, c | |
| y = line_line_intersection(Line(e, f), Line(g, h)) | |
| if f.distance(y) > e.distance(y): | |
| e, f = f, e | |
| if h.distance(y) > g.distance(y): | |
| g, h = h, g | |
| e, f, h = y, e, g | |
| _draw_line(ax, a, b, color=lcolor, lw=2.0) | |
| _draw_line(ax, a, d, color=lcolor, lw=2.0) | |
| _draw_line(ax, e, f, color=lcolor, lw=2.0) | |
| _draw_line(ax, e, h, color=lcolor, lw=2.0) | |
| if color1 == '--': | |
| color1 = 'red' | |
| draw_angle(ax, a, b, d, color=color1, alpha=0.5) | |
| if color2 == '--': | |
| color2 = 'red' | |
| draw_angle(ax, e, f, h, color=color2, alpha=0.5) | |
| if name == 'perp': | |
| a, b, c, d = args | |
| _draw_line(ax, a, b, color=color1, lw=2.0) | |
| _draw_line(ax, c, d, color=color1, lw=2.0) | |
| if name == 'ratio': | |
| a, b, c, d, m, n = args | |
| _draw_line(ax, a, b, color=color1, lw=2.0) | |
| _draw_line(ax, c, d, color=color2, lw=2.0) | |
| if name == 'cong': | |
| a, b, c, d = args | |
| _draw_line(ax, a, b, color=color1, lw=2.0) | |
| _draw_line(ax, c, d, color=color2, lw=2.0) | |
| if name == 'midp': | |
| m, a, b = args | |
| _draw_line(ax, a, m, color=color1, lw=2.0, alpha=0.5) | |
| _draw_line(ax, b, m, color=color2, lw=2.0, alpha=0.5) | |
| if name == 'eqratio': | |
| a, b, c, d, m, n, p, q = args | |
| _draw_line(ax, a, b, color=color1, lw=2.0, alpha=0.5) | |
| _draw_line(ax, c, d, color=color2, lw=2.0, alpha=0.5) | |
| _draw_line(ax, m, n, color=color1, lw=2.0, alpha=0.5) | |
| _draw_line(ax, p, q, color=color2, lw=2.0, alpha=0.5) | |
| if name == 'iso_triangle': | |
| a, b, c = args | |
| _draw_line(ax, c, b, color=color1, lw=2.0) | |
| _draw_line(ax, c, a, color=color1, lw=2.0) | |
| _draw_line(ax, b, a, color=color2, lw=2.0) | |
| if name == 'semicircle': | |
| o, a, b, c = args | |
| _draw_semicircle(ax, SemiCircle(center=o, p1=a, p2=b, p3=c), color=color1, lw=2.0) | |
| def convert_point(gm_point: gm.Point) -> Point: | |
| return Point(gm_point.num.x, gm_point.num.y) | |
| HCOLORS = None | |
| def find_pairs_with_same_distance(line_lengths): | |
| # Step 1: Group point pairs by distance | |
| distance_groups = defaultdict(list) | |
| for (p1, p2), distance in line_lengths.items(): | |
| distance_groups[distance].append((p1, p2)) | |
| # Step 2: Find combinations of pairs with the same distance | |
| result = [] | |
| for pairs in distance_groups.values(): | |
| if len(pairs) > 1: # Only consider distances with multiple pairs | |
| for i in range(len(pairs)): | |
| for j in range(i + 1, len(pairs)): | |
| line1 = pairs[i] | |
| line2 = pairs[j] | |
| result.append((line1, line2)) # Group as ((p1, p2), (p3, p4)) | |
| return result | |
| def calculate_angle(p1, p2, p3, p4): | |
| """Calculates the angle between two lines formed by points (p1, p2) and (p3, p4) in degrees.""" | |
| # Determine the common point | |
| if p2 == p3: | |
| common = p2 | |
| other_points = (p1, p4) | |
| v1 = np.array([p1.x - p2.x, p1.y - p2.y]) | |
| v2 = np.array([p4.x - p2.x, p4.y - p2.y]) | |
| elif p1 == p4: | |
| common = p1 | |
| other_points = (p2, p3) | |
| v1 = np.array([p2.x - p1.x, p2.y - p1.y]) | |
| v2 = np.array([p3.x - p1.x, p3.y - p1.y]) | |
| elif p1 == p3: | |
| common = p1 | |
| other_points = (p2, p4) | |
| v1 = np.array([p2.x - p1.x, p2.y - p1.y]) | |
| v2 = np.array([p4.x - p1.x, p4.y - p1.y]) | |
| elif p2 == p4: | |
| common = p2 | |
| other_points = (p1, p3) | |
| v1 = np.array([p1.x - p2.x, p1.y - p2.y]) | |
| v2 = np.array([p3.x - p2.x, p3.y - p2.y]) | |
| else: | |
| return None, None, None # No shared point, angle cannot be calculated | |
| # Calculate the angle | |
| cos_angle = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2)) | |
| cos_angle = np.clip(cos_angle, -1, 1) # Ensure valid range for acos | |
| angle_rad = np.arccos(cos_angle) | |
| return common, other_points, round(np.degrees(angle_rad), 5) | |
| def highlight_angle2(ax, origin, p1, p2, radius, color): | |
| """Highlights the angle formed by two vectors meeting at 'origin'.""" | |
| # Calculate angles of vectors | |
| angle1 = np.arctan2(p1.y - origin.y, p1.x - origin.x) | |
| angle2 = np.arctan2(p2.y - origin.y, p2.x - origin.x) | |
| # Convert to degrees and ensure the smaller angle is first | |
| angle1_deg, angle2_deg = sorted(np.degrees([angle1, angle2])) | |
| if angle2_deg - angle1_deg > 180: | |
| angle1_deg, angle2_deg = angle2_deg, angle1_deg | |
| # Draw the wedge | |
| wedge = matplotlib.patches.Wedge( | |
| center=(origin.x, origin.y), | |
| r=radius, | |
| theta1=angle1_deg, | |
| theta2=angle2_deg, | |
| color=color, | |
| alpha=0.5 | |
| ) | |
| ax.add_patch(wedge) | |
| # print("Angle highlighted with color:", color) | |
| def search_in_dict(num, my_dict): | |
| for key in my_dict.keys(): | |
| if round(key, 3) == round(num, 3): | |
| return True | |
| return False | |
| def highlight_same_angle(ax, lines, color_list): | |
| """Highlights angles formed at shared points by pairs of lines.""" | |
| lines_list = [(draw_line(ax, l, draw=False)) for l in lines] # Extract points for all lines | |
| angle_color_radius = {} | |
| for line1, line2 in combinations(lines_list, 2): | |
| # Calculate the angle | |
| common_point, other_points, angle = calculate_angle(*line1, *line2) | |
| if angle is None or angle > 90: | |
| continue # Skip invalid or small angles | |
| if search_in_dict(angle, angle_color_radius) == False: | |
| # Assign color and radius for this unique angle | |
| color = color_list[len(angle_color_radius) % len(color_list)] | |
| radius = 0.1 + len(angle_color_radius) * 0.05 | |
| angle_color_radius[round(angle,3)] = (color, radius) | |
| # print(type(angle)) | |
| else: | |
| color, radius = angle_color_radius[round(angle, 3)] | |
| # print(type(angle)) | |
| # Highlight the angle | |
| highlight_angle2(ax, common_point, *other_points, radius, color) | |
| def _draw( | |
| ax: matplotlib.axes.Axes, | |
| points: list[gm.Point], | |
| lines: list[gm.Line], | |
| circles: list[gm.Circle], | |
| semicircles: list[gm.SemiCircle], | |
| goal: Any, | |
| equals: list[tuple[Any, Any]], | |
| highlights: list[tuple[str, list[gm.Point]]], | |
| ): | |
| """Draw everything.""" | |
| colors = ['red', 'green', 'blue', 'orange', 'magenta', 'purple'] | |
| colors_highlight = [color for color in mcolors.TABLEAU_COLORS.keys() if color not in colors] | |
| pcolor = 'black' | |
| lcolor = 'black' | |
| ccolor = 'grey' | |
| if get_theme() == 'dark': | |
| pcolor, lcolor, ccolor = 'white', 'white', 'cyan' | |
| elif get_theme() == 'light': | |
| pcolor, lcolor, ccolor = 'black', 'black', 'blue' | |
| elif get_theme() == 'grey': | |
| pcolor, lcolor, ccolor = 'black', 'black', 'grey' | |
| colors = ['grey'] | |
| line_boundaries = [] | |
| line_lengths = {} | |
| # Convert all points | |
| for p in points: | |
| points_numericals = [convert_point(p) for p in points] | |
| for l in lines: | |
| p1, p2 = draw_line(ax, l, color=lcolor) | |
| line_boundaries.append((p1, p2)) | |
| points_numericals.append(p1) | |
| points_numericals.append(p2) | |
| unique_points = [] | |
| seen = set() | |
| for p in points_numericals: | |
| if (p.x, p.y) not in seen: | |
| unique_points.append(p) | |
| seen.add((p.x, p.y)) | |
| print(len(unique_points)) | |
| for p1, p2 in combinations(unique_points, 2): | |
| line_lengths[(p1, p2)] = round(p1.distance(p2), 9) | |
| circles = [draw_circle(ax, c, color=ccolor) for c in circles] | |
| semicircles = [draw_semicircle(ax, c, color=ccolor) for c in semicircles] | |
| for p in points: | |
| draw_point(ax, p.num, p.name, line_boundaries, circles, semicircles, color=pcolor) | |
| same_length_pairs = find_pairs_with_same_distance(line_lengths) | |
| print(len(same_length_pairs)) | |
| length_color_map = {} # Dictionary to map length to its color | |
| for i, ((p1, p2), (p3, p4)) in enumerate(same_length_pairs): | |
| line_length = p1.distance(p2) # Calculate the length of the line | |
| if line_length not in length_color_map: # Check if length is already in the dictionary | |
| #color = colors_highlight[i % len(colors_highlight)] | |
| color = colors_highlight[i % len(colors_highlight) ] # Assign a new color | |
| length_color_map[line_length] = color # Store the length and color in the dictionary | |
| else: | |
| color = length_color_map[line_length] # Use the existing color for this length | |
| # Call the highlight function with the determined color | |
| highlight(ax, 'cong', [p1, p2, p3, p4], lcolor, color, color) | |
| highlight_same_angle(ax, lines, color_list=colors_highlight) | |
| if equals: | |
| for i, segs in enumerate(equals['segments']): | |
| color = colors[i % len(colors)] | |
| for a, b in segs: | |
| mark_segment(ax, a, b, color, 0.5) | |
| for i, angs in enumerate(equals['angles']): | |
| color = colors[i % len(colors)] | |
| for a, b, c, d in angs: | |
| highlight_angle(ax, a, b, c, d, color, 0.5) | |
| if highlights: | |
| global HCOLORS | |
| if HCOLORS is None: | |
| HCOLORS = [k for k in mcolors.TABLEAU_COLORS.keys() if 'red' not in k] | |
| for i, (name, args) in enumerate(highlights): | |
| color_i = HCOLORS[i % len(HCOLORS)] | |
| highlight(ax, name, args, 'black', color_i, color_i) | |
| if goal: | |
| name, args = goal | |
| lcolor = color1 = color2 = 'red' | |
| highlight(ax, name, args, lcolor, color1, color2) | |
| THEME = 'dark' | |
| def set_theme(theme) -> None: | |
| global THEME | |
| THEME = theme | |
| def get_theme() -> str: | |
| return THEME | |
| def draw( | |
| points: list[gm.Point], | |
| lines: list[gm.Line], | |
| circles: list[gm.Circle], | |
| semicircles: list[gm.SemiCircle], | |
| segments: list[gm.Segment], | |
| goal: Any = None, | |
| highlights: list[tuple[str, list[gm.Point]]] = None, | |
| equals: list[tuple[Any, Any]] = None, | |
| block: bool = True, | |
| save_to: str = None, | |
| theme: str = 'dark', | |
| ) -> None: | |
| """Draw everything on the same canvas.""" | |
| plt.close() | |
| imsize = 1280 / 200 | |
| fig, ax = plt.subplots(figsize=(imsize, imsize), dpi=200) | |
| set_theme(theme) | |
| if get_theme() == 'dark': | |
| ax.set_facecolor((0.0, 0.0, 0.0)) | |
| else: | |
| ax.set_facecolor((1.0, 1.0, 1.0)) | |
| _draw(ax, points, lines, circles, semicircles, goal, equals, highlights) | |
| plt.axis('equal') | |
| fig.subplots_adjust(left=0, right=1, top=1, bottom=0, wspace=0, hspace=0) | |
| if points: | |
| xmin = min([p.num.x for p in points]) | |
| xmax = max([p.num.x for p in points]) | |
| ymin = min([p.num.y for p in points]) | |
| ymax = max([p.num.y for p in points]) | |
| plt.margins((xmax - xmin) * 0.1, (ymax - ymin) * 0.1) | |
| if save_to: | |
| plt.savefig(save_to) | |
| # plt.show(block=block) | |
| def close_enough(a: float, b: float, tol: float = 1e-12) -> bool: | |
| return abs(a - b) < tol | |
| def assert_close_enough(a: float, b: float, tol: float = 1e-12) -> None: | |
| assert close_enough(a, b, tol), f'|{a}-{b}| = {abs(a-b)} >= {tol}' | |
| def ang_of(tail: Point, head: Point) -> float: | |
| vector = head - tail | |
| arctan = np.arctan2(vector.y, vector.x) % (2 * np.pi) | |
| return arctan | |
| def ang_between(tail: Point, head1: Point, head2: Point) -> float: | |
| ang1 = ang_of(tail, head1) | |
| ang2 = ang_of(tail, head2) | |
| diff = ang1 - ang2 | |
| # return diff % (2*np.pi) | |
| if diff > np.pi: | |
| return diff - 2 * np.pi | |
| if diff < -np.pi: | |
| return 2 * np.pi + diff | |
| return diff | |
| def head_from(tail: Point, ang: float, length: float = 1) -> Point: | |
| vector = Point(np.cos(ang) * length, np.sin(ang) * length) | |
| return tail + vector | |
| def random_points(n: int = 3) -> list[Point]: | |
| return [Point(unif(-1, 1), unif(-1, 1)) for _ in range(n)] | |
| def random_rfss(*points: list[Point]) -> list[Point]: | |
| """Random rotate-flip-scale-shift a point cloud.""" | |
| # center point cloud. | |
| average = sum(points, Point(0.0, 0.0)) * (1.0 / len(points)) | |
| points = [p - average for p in points] | |
| # rotate | |
| ang = unif(0.0, 2 * np.pi) | |
| sin, cos = np.sin(ang), np.cos(ang) | |
| # scale and shift | |
| scale = unif(0.5, 2.0) | |
| shift = Point(unif(-1, 1), unif(-1, 1)) | |
| points = [p.rotate(sin, cos) * scale + shift for p in points] | |
| # randomly flip | |
| if np.random.rand() < 0.5: | |
| points = [p.flip() for p in points] | |
| return points | |
| def reduce( | |
| objs: list[Union[Point, Line, Circle, HalfLine, HoleCircle]], | |
| existing_points: list[Point], | |
| ) -> list[Point]: | |
| """Reduce intersecting objects into one point of intersections.""" | |
| if all(isinstance(o, Point) for o in objs): | |
| return objs | |
| elif len(objs) == 1: | |
| return objs[0].sample_within(existing_points) | |
| elif len(objs) == 2: | |
| a, b = objs | |
| result = a.intersect(b) | |
| if isinstance(result, Point): | |
| return [result] | |
| a, b = result | |
| a_close = any([a.close(x) for x in existing_points]) | |
| if a_close: | |
| return [b] | |
| b_close = any([b.close(x) for x in existing_points]) | |
| if b_close: | |
| return [a] | |
| return [np.random.choice([a, b])] | |
| else: | |
| raise ValueError(f'Cannot reduce {objs}') | |
| def sketch( | |
| name: str, args: list[Union[Point, gm.Point]] | |
| ) -> list[Union[Point, Line, Circle, HalfLine, HoleCircle]]: | |
| fun = globals()['sketch_' + name] | |
| args = [p.num if isinstance(p, gm.Point) else p for p in args] | |
| out = fun(args) | |
| # out can be one or multiple {Point/Line/HalfLine} | |
| if isinstance(out, (tuple, list)): | |
| return list(out) | |
| return [out] | |
| def sketch_on_opline(args: tuple[gm.Point, ...]) -> HalfLine: | |
| a, b = args | |
| return HalfLine(a, a + a - b) | |
| def sketch_on_hline(args: tuple[gm.Point, ...]) -> HalfLine: | |
| a, b = args | |
| return HalfLine(a, b) | |
| def sketch_ieq_triangle(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| a = Point(0.0, 0.0) | |
| b = Point(1.0, 0.0) | |
| c, _ = Circle(a, p1=b).intersect(Circle(b, p1=a)) | |
| return a, b, c | |
| def sketch_incenter2(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| a, b, c = args | |
| l1 = sketch_bisect([b, a, c]) | |
| l2 = sketch_bisect([a, b, c]) | |
| i = line_line_intersection(l1, l2) | |
| x = i.foot(Line(b, c)) | |
| y = i.foot(Line(c, a)) | |
| z = i.foot(Line(a, b)) | |
| return x, y, z, i | |
| def sketch_excenter2(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| a, b, c = args | |
| l1 = sketch_bisect([b, a, c]) | |
| l2 = sketch_exbisect([a, b, c]) | |
| i = line_line_intersection(l1, l2) | |
| x = i.foot(Line(b, c)) | |
| y = i.foot(Line(c, a)) | |
| z = i.foot(Line(a, b)) | |
| return x, y, z, i | |
| def sketch_centroid(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| a, b, c = args | |
| x = (b + c) * 0.5 | |
| y = (c + a) * 0.5 | |
| z = (a + b) * 0.5 | |
| i = line_line_intersection(Line(a, x), Line(b, y)) | |
| return x, y, z, i | |
| def sketch_ninepoints(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| a, b, c = args | |
| x = (b + c) * 0.5 | |
| y = (c + a) * 0.5 | |
| z = (a + b) * 0.5 | |
| c = Circle(p1=x, p2=y, p3=z) | |
| return x, y, z, c.center | |
| def sketch_2l1c(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| """Sketch a circle touching two lines and another circle.""" | |
| a, b, c, p = args | |
| bc, ac = Line(b, c), Line(a, c) | |
| circle = Circle(p, p1=a) | |
| d, d_ = line_circle_intersection(p.perpendicular_line(bc), circle) | |
| if bc.diff_side(d_, a): | |
| d = d_ | |
| e, e_ = line_circle_intersection(p.perpendicular_line(ac), circle) | |
| if ac.diff_side(e_, b): | |
| e = e_ | |
| df = d.perpendicular_line(Line(p, d)) | |
| ef = e.perpendicular_line(Line(p, e)) | |
| f = line_line_intersection(df, ef) | |
| g, g_ = line_circle_intersection(Line(c, f), circle) | |
| if bc.same_side(g_, a): | |
| g = g_ | |
| b_ = c + (b - c) / b.distance(c) | |
| a_ = c + (a - c) / a.distance(c) | |
| m = (a_ + b_) * 0.5 | |
| x = line_line_intersection(Line(c, m), Line(p, g)) | |
| return x.foot(ac), x.foot(bc), g, x | |
| def sketch_3peq(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| a, b, c = args | |
| ab, bc, ca = Line(a, b), Line(b, c), Line(c, a) | |
| z = b + (c - b) * np.random.uniform(-0.5, 1.5) | |
| z_ = z * 2 - c | |
| l = z_.parallel_line(ca) | |
| x = line_line_intersection(l, ab) | |
| y = z * 2 - x | |
| return x, y, z | |
| def try_to_sketch_intersect( | |
| name1: str, | |
| args1: list[Union[gm.Point, Point]], | |
| name2: str, | |
| args2: list[Union[gm.Point, Point]], | |
| existing_points: list[Point], | |
| ) -> Optional[Point]: | |
| """Try to sketch an intersection between two objects.""" | |
| obj1 = sketch(name1, args1)[0] | |
| obj2 = sketch(name2, args2)[0] | |
| if isinstance(obj1, Line) and isinstance(obj2, Line): | |
| fn = line_line_intersection | |
| elif isinstance(obj1, Circle) and isinstance(obj2, Circle): | |
| fn = circle_circle_intersection | |
| else: | |
| fn = line_circle_intersection | |
| if isinstance(obj2, Line) and isinstance(obj1, Circle): | |
| obj1, obj2 = obj2, obj1 | |
| try: | |
| x = fn(obj1, obj2) | |
| except: # pylint: disable=bare-except | |
| return None | |
| if isinstance(x, Point): | |
| return x | |
| x1, x2 = x | |
| close1 = check_too_close([x1], existing_points) | |
| far1 = check_too_far([x1], existing_points) | |
| if not close1 and not far1: | |
| return x1 | |
| close2 = check_too_close([x2], existing_points) | |
| far2 = check_too_far([x2], existing_points) | |
| if not close2 and not far2: | |
| return x2 | |
| return None | |
| def sketch_acircle(args: tuple[gm.Point, ...]) -> Circle: | |
| a, b, c, d, f = args | |
| de = sketch_aline([c, a, b, f, d]) | |
| fe = sketch_aline([a, c, b, d, f]) | |
| e = line_line_intersection(de, fe) | |
| return Circle(p1=d, p2=e, p3=f) | |
| def sketch_aline(args: tuple[gm.Point, ...]) -> HalfLine: | |
| """Sketch the construction aline.""" | |
| A, B, C, D, E = args | |
| ab = A - B | |
| cb = C - B | |
| de = D - E | |
| dab = A.distance(B) | |
| ang_ab = np.arctan2(ab.y / dab, ab.x / dab) | |
| dcb = C.distance(B) | |
| ang_bc = np.arctan2(cb.y / dcb, cb.x / dcb) | |
| dde = D.distance(E) | |
| ang_de = np.arctan2(de.y / dde, de.x / dde) | |
| ang_ex = ang_de + ang_bc - ang_ab | |
| X = E + Point(np.cos(ang_ex), np.sin(ang_ex)) | |
| return HalfLine(E, X) | |
| def sketch_amirror(args: tuple[gm.Point, ...]) -> HalfLine: | |
| """Sketch the angle mirror.""" | |
| A, B, C = args # pylint: disable=invalid-name | |
| ab = A - B | |
| cb = C - B | |
| dab = A.distance(B) | |
| ang_ab = np.arctan2(ab.y / dab, ab.x / dab) | |
| dcb = C.distance(B) | |
| ang_bc = np.arctan2(cb.y / dcb, cb.x / dcb) | |
| ang_bx = 2 * ang_bc - ang_ab | |
| X = B + Point(np.cos(ang_bx), np.sin(ang_bx)) # pylint: disable=invalid-name | |
| return HalfLine(B, X) | |
| def sketch_bisect(args: tuple[gm.Point, ...]) -> Line: | |
| a, b, c = args | |
| ab = a.distance(b) | |
| bc = b.distance(c) | |
| x = b + (c - b) * (ab / bc) | |
| m = (a + x) * 0.5 | |
| return Line(b, m) | |
| def sketch_exbisect(args: tuple[gm.Point, ...]) -> Line: | |
| a, b, c = args | |
| return sketch_bisect(args).perpendicular_line(b) | |
| def sketch_bline(args: tuple[gm.Point, ...]) -> Line: | |
| a, b = args | |
| m = (a + b) * 0.5 | |
| return m.perpendicular_line(Line(a, b)) | |
| def sketch_dia(args: tuple[gm.Point, ...]) -> Circle: | |
| a, b = args | |
| return Circle((a + b) * 0.5, p1=a) | |
| def sketch_tangent(args: tuple[gm.Point, ...]) -> tuple[Point, Point]: | |
| a, o, b = args | |
| dia = sketch_dia([a, o]) | |
| return circle_circle_intersection(Circle(o, p1=b), dia) | |
| def sketch_circle(args: tuple[gm.Point, ...]) -> Circle: | |
| a, b, c = args | |
| return Circle(center=a, radius=b.distance(c)) | |
| def sketch_semicircle(args: tuple[gm.Point, ...]) -> SemiCircle: | |
| a, b, c = args | |
| return SemiCircle(center=a, radius=b.distance(c), p1=b, p2=c) | |
| def sketch_cc_tangent(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| """Sketch tangents to two circles.""" | |
| o, a, w, b = args | |
| ra, rb = o.distance(a), w.distance(b) | |
| ow = Line(o, w) | |
| if close_enough(ra, rb): | |
| oo = ow.perpendicular_line(o) | |
| oa = Circle(o, ra) | |
| x, z = line_circle_intersection(oo, oa) | |
| y = x + w - o | |
| t = z + w - o | |
| return x, y, z, t | |
| swap = rb > ra | |
| if swap: | |
| o, a, w, b = w, b, o, a | |
| ra, rb = rb, ra | |
| oa = Circle(o, ra) | |
| q = o + (w - o) * ra / (ra - rb) | |
| x, z = circle_circle_intersection(sketch_dia([o, q]), oa) | |
| y = w.foot(Line(x, q)) | |
| t = w.foot(Line(z, q)) | |
| if swap: | |
| x, y, z, t = y, x, t, z | |
| return x, y, z, t | |
| def sketch_hcircle(args: tuple[gm.Point, ...]) -> HoleCircle: | |
| a, b = args | |
| return HoleCircle(center=a, radius=a.distance(b), hole=b) | |
| def sketch_e5128(args: tuple[gm.Point, ...]) -> tuple[Point, Point]: | |
| a, b, c, d = args | |
| ad = Line(a, d) | |
| g = (a + b) * 0.5 | |
| de = Line(d, g) | |
| e, f = line_circle_intersection(de, Circle(c, p1=b)) | |
| if e.distance(d) < f.distance(d): | |
| e = f | |
| return e, g | |
| def sketch_eq_quadrangle(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| """Sketch quadrangle with two equal opposite sides.""" | |
| a = Point(0.0, 0.0) | |
| b = Point(1.0, 0.0) | |
| length = np.random.uniform(0.5, 2.0) | |
| ang = np.random.uniform(np.pi / 3, np.pi * 2 / 3) | |
| d = head_from(a, ang, length) | |
| ang = ang_of(b, d) | |
| ang = np.random.uniform(ang / 10, ang / 9) | |
| c = head_from(b, ang, length) | |
| a, b, c, d = random_rfss(a, b, c, d) | |
| return a, b, c, d | |
| def sketch_eq_trapezoid(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| a = Point(0.0, 0.0) | |
| b = Point(1.0, 0.0) | |
| l = unif(0.5, 2.0) | |
| height = unif(0.5, 2.0) | |
| c = Point(0.5 + l / 2.0, height) | |
| d = Point(0.5 - l / 2.0, height) | |
| a, b, c, d = random_rfss(a, b, c, d) | |
| return a, b, c, d | |
| def sketch_eqangle2(args: tuple[gm.Point, ...]) -> Point: | |
| """Sketch the def eqangle2.""" | |
| a, b, c = args | |
| d = c * 2 - b | |
| ba = b.distance(a) | |
| bc = b.distance(c) | |
| l = ba * ba / bc | |
| if unif(0.0, 1.0) < 0.5: | |
| be = min(l, bc) | |
| be = unif(be * 0.1, be * 0.9) | |
| else: | |
| be = max(l, bc) | |
| be = unif(be * 1.1, be * 1.5) | |
| e = b + (c - b) * (be / bc) | |
| y = b + (a - b) * (be / l) | |
| return line_line_intersection(Line(c, y), Line(a, e)) | |
| def sketch_eqangle3(args: tuple[gm.Point, ...]) -> Circle: | |
| a, b, d, e, f = args | |
| de = d.distance(e) | |
| ef = e.distance(f) | |
| ab = b.distance(a) | |
| ang_ax = ang_of(a, b) + ang_between(e, d, f) | |
| x = head_from(a, ang_ax, length=de / ef * ab) | |
| return Circle(p1=a, p2=b, p3=x) | |
| def sketch_eqdia_quadrangle(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| """Sketch quadrangle with two equal diagonals.""" | |
| m = unif(0.3, 0.7) | |
| n = unif(0.3, 0.7) | |
| a = Point(-m, 0.0) | |
| c = Point(1 - m, 0.0) | |
| b = Point(0.0, -n) | |
| d = Point(0.0, 1 - n) | |
| ang = unif(-0.25 * np.pi, 0.25 * np.pi) | |
| sin, cos = np.sin(ang), np.cos(ang) | |
| b = b.rotate(sin, cos) | |
| d = d.rotate(sin, cos) | |
| a, b, c, d = random_rfss(a, b, c, d) | |
| return a, b, c, d | |
| def sketch_free(args: tuple[gm.Point, ...]) -> Point: | |
| return random_points(1)[0] | |
| def sketch_isos(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| base = unif(0.5, 1.5) | |
| height = unif(0.5, 1.5) | |
| b = Point(-base / 2, 0.0) | |
| c = Point(base / 2, 0.0) | |
| a = Point(0.0, height) | |
| a, b, c = random_rfss(a, b, c) | |
| return a, b, c | |
| def sketch_line(args: tuple[gm.Point, ...]) -> Line: | |
| a, b = args | |
| return Line(a, b) | |
| def sketch_cyclic(args: tuple[gm.Point, ...]) -> Circle: | |
| a, b, c = args | |
| return Circle(p1=a, p2=b, p3=c) | |
| def sketch_hline(args: tuple[gm.Point, ...]) -> HalfLine: | |
| a, b = args | |
| return HalfLine(a, b) | |
| def sketch_midp(args: tuple[gm.Point, ...]) -> Point: | |
| a, b = args | |
| return (a + b) * 0.5 | |
| def sketch_foot(args: tuple[gm.Point, ...]) -> Point: | |
| a, b, c = args | |
| return a.foot(Line(b, c)) | |
| def sketch_pentagon(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| points = [Point(1.0, 0.0)] | |
| ang = 0.0 | |
| for i in range(4): | |
| ang += (2 * np.pi - ang) / (5 - i) * unif(0.5, 1.5) | |
| point = Point(np.cos(ang), np.sin(ang)) | |
| points.append(point) | |
| a, b, c, d, e = points # pylint: disable=unbalanced-tuple-unpacking | |
| a, b, c, d, e = random_rfss(a, b, c, d, e) | |
| return a, b, c, d, e | |
| def sketch_pline(args: tuple[gm.Point, ...]) -> Line: | |
| a, b, c = args | |
| return a.parallel_line(Line(b, c)) | |
| def sketch_pmirror(args: tuple[gm.Point, ...]) -> Point: | |
| a, b = args | |
| return b * 2 - a | |
| def sketch_quadrangle(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| """Sketch a random quadrangle.""" | |
| m = unif(0.3, 0.7) | |
| n = unif(0.3, 0.7) | |
| a = Point(-m, 0.0) | |
| c = Point(1 - m, 0.0) | |
| b = Point(0.0, -unif(0.25, 0.75)) | |
| d = Point(0.0, unif(0.25, 0.75)) | |
| ang = unif(-0.25 * np.pi, 0.25 * np.pi) | |
| sin, cos = np.sin(ang), np.cos(ang) | |
| b = b.rotate(sin, cos) | |
| d = d.rotate(sin, cos) | |
| a, b, c, d = random_rfss(a, b, c, d) | |
| return a, b, c, d | |
| def sketch_r_trapezoid(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| a = Point(0.0, 1.0) | |
| d = Point(0.0, 0.0) | |
| b = Point(unif(0.5, 1.5), 1.0) | |
| c = Point(unif(0.5, 1.5), 0.0) | |
| a, b, c, d = random_rfss(a, b, c, d) | |
| return a, b, c, d | |
| def sketch_r_triangle(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| a = Point(0.0, 0.0) | |
| b = Point(0.0, unif(0.5, 2.0)) | |
| c = Point(unif(0.5, 2.0), 0.0) | |
| a, b, c = random_rfss(a, b, c) | |
| return a, b, c | |
| def sketch_rectangle(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| a = Point(0.0, 0.0) | |
| b = Point(0.0, 1.0) | |
| l = unif(0.5, 2.0) | |
| c = Point(l, 1.0) | |
| d = Point(l, 0.0) | |
| a, b, c, d = random_rfss(a, b, c, d) | |
| return a, b, c, d | |
| def sketch_reflect(args: tuple[gm.Point, ...]) -> Point: | |
| a, b, c = args | |
| m = a.foot(Line(b, c)) | |
| return m * 2 - a | |
| def sketch_risos(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| a = Point(0.0, 0.0) | |
| b = Point(0.0, 1.0) | |
| c = Point(1.0, 0.0) | |
| a, b, c = random_rfss(a, b, c) | |
| return a, b, c | |
| def sketch_rotaten90(args: tuple[gm.Point, ...]) -> Point: | |
| a, b = args | |
| ang = -np.pi / 2 | |
| return a + (b - a).rotate(np.sin(ang), np.cos(ang)) | |
| def sketch_rotatep90(args: tuple[gm.Point, ...]) -> Point: | |
| a, b = args | |
| ang = np.pi / 2 | |
| return a + (b - a).rotate(np.sin(ang), np.cos(ang)) | |
| def sketch_s_angle(args: tuple[gm.Point, ...]) -> HalfLine: | |
| a, b, y = args | |
| ang = y / 180 * np.pi | |
| x = b + (a - b).rotatea(ang) | |
| return HalfLine(b, x) | |
| def sketch_segment(args: tuple[gm.Point, ...]) -> tuple[Point, Point]: | |
| a, b = random_points(2) | |
| return a, b | |
| def sketch_shift(args: tuple[gm.Point, ...]) -> Point: | |
| a, b, c = args | |
| return c + (b - a) | |
| def sketch_square(args: tuple[gm.Point, ...]) -> tuple[Point, Point]: | |
| a, b = args | |
| c = b + (a - b).rotatea(-np.pi / 2) | |
| d = a + (b - a).rotatea(np.pi / 2) | |
| return c, d | |
| def sketch_isquare(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| a = Point(0.0, 0.0) | |
| b = Point(1.0, 0.0) | |
| c = Point(1.0, 1.0) | |
| d = Point(0.0, 1.0) | |
| a, b, c, d = random_rfss(a, b, c, d) | |
| return a, b, c, d | |
| def sketch_tline(args: tuple[gm.Point, ...]) -> Line: | |
| a, b, c = args | |
| return a.perpendicular_line(Line(b, c)) | |
| def sketch_trapezoid(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| d = Point(0.0, 0.0) | |
| c = Point(1.0, 0.0) | |
| base = unif(0.5, 2.0) | |
| height = unif(0.5, 2.0) | |
| a = Point(unif(0.2, 0.5), height) | |
| b = Point(a.x + base, height) | |
| a, b, c, d = random_rfss(a, b, c, d) | |
| return a, b, c, d | |
| def sketch_triangle(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| a = Point(0.0, 0.0) | |
| b = Point(1.0, 0.0) | |
| ac = unif(0.5, 2.0) | |
| ang = unif(0.2, 0.8) * np.pi | |
| c = head_from(a, ang, ac) | |
| return a, b, c | |
| def sketch_triangle12(args: tuple[gm.Point, ...]) -> tuple[Point, ...]: | |
| b = Point(0.0, 0.0) | |
| c = Point(unif(1.5, 2.5), 0.0) | |
| a, _ = circle_circle_intersection(Circle(b, 1.0), Circle(c, 2.0)) | |
| a, b, c = random_rfss(a, b, c) | |
| return a, b, c | |
| def sketch_trisect(args: tuple[gm.Point, ...]) -> tuple[Point, Point]: | |
| """Sketch two trisectors of an angle.""" | |
| a, b, c = args | |
| ang1 = ang_of(b, a) | |
| ang2 = ang_of(b, c) | |
| swap = 0 | |
| if ang1 > ang2: | |
| ang1, ang2 = ang2, ang1 | |
| swap += 1 | |
| if ang2 - ang1 > np.pi: | |
| ang1, ang2 = ang2, ang1 + 2 * np.pi | |
| swap += 1 | |
| angx = ang1 + (ang2 - ang1) / 3 | |
| angy = ang2 - (ang2 - ang1) / 3 | |
| x = b + Point(np.cos(angx), np.sin(angx)) | |
| y = b + Point(np.cos(angy), np.sin(angy)) | |
| ac = Line(a, c) | |
| x = line_line_intersection(Line(b, x), ac) | |
| y = line_line_intersection(Line(b, y), ac) | |
| if swap == 1: | |
| return y, x | |
| return x, y | |
| def sketch_trisegment(args: tuple[gm.Point, ...]) -> tuple[Point, Point]: | |
| a, b = args | |
| x, y = a + (b - a) * (1.0 / 3), a + (b - a) * (2.0 / 3) | |
| return x, y | |