Spaces:
Sleeping
Sleeping
# 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 | |