HugoVoxx commited on
Commit
630bb62
·
verified ·
1 Parent(s): 2091157

Upload 2 files

Browse files
ag4masses/alphageometry/graph.py CHANGED
@@ -2773,7 +2773,7 @@ class Graph:
2773
 
2774
  # not necessary for proofing, but for visualization.
2775
  c_args = list(map(lambda x: self.get(x, lambda: int(x)), c.args))
2776
- self.additionally_draw(c.name, c_args)
2777
 
2778
  for points, bs in cdef.basics:
2779
  if points:
 
2773
 
2774
  # not necessary for proofing, but for visualization.
2775
  c_args = list(map(lambda x: self.get(x, lambda: int(x)), c.args))
2776
+ # self.additionally_draw(c.name, c_args)
2777
 
2778
  for points, bs in cdef.basics:
2779
  if points:
ag4masses/alphageometry/numericals.py CHANGED
@@ -18,6 +18,7 @@ from __future__ import annotations
18
 
19
  import math
20
  from typing import Any, Optional, Union
 
21
 
22
  import geometry as gm
23
  import matplotlib
@@ -26,8 +27,10 @@ import matplotlib.colors as mcolors
26
  import numpy as np
27
  from numpy.random import uniform as unif # pylint: disable=g-importing-member
28
  import graph as gh
 
 
29
 
30
- matplotlib.use('Agg')
31
 
32
 
33
  ATOM = 1e-12
@@ -440,72 +443,72 @@ class Circle:
440
 
441
 
442
  class SemiCircle(Circle):
443
- """Numerical semicircle, inherits from Circle."""
444
-
445
- def __init__(
446
- self,
447
- center: Optional[Point] = None,
448
- radius: Optional[float] = None,
449
- p1: Optional[Point] = None,
450
- p2: Optional[Point] = None,
451
- p3: Optional[Point] = None,
452
- ):
453
- self.p1 = p1
454
- self.p2 = p2
455
- self.p3 = p3
456
- # Initialize as a Circle
457
- super().__init__(center, radius, p1, p2, p3)
458
- # If p1 and p2 define a diameter, set the center and radius accordingly
459
- if p1 and p2 and not center:
460
- self.center = Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2)
461
- self.radius = p1.distance(p2) / 2
462
- self.r2 = self.radius ** 2
463
-
464
- # Define the direction or plane for the semicircle (important for sampling and boundaries)
465
-
466
- def is_within_boundary(self, point: Point) -> bool:
467
- """Check if a point is within the boundary of the semicircle."""
468
- vector_to_point = point - self.center
469
- angle = math.atan2(vector_to_point.y, vector_to_point.x)
470
-
471
- # Normalize the angle within [0, 2*pi]
472
- angle = angle if angle >= 0 else (2 * np.pi + angle)
473
-
474
- # Check if the point is within the semicircle (half of the circle)
475
- return -np.pi / 2 <= angle <= np.pi / 2
476
-
477
- def sample_within(self, points: list[Point], n: int = 5) -> list[Point]:
478
- """Sample a point within the semicircle."""
479
- result = None
480
- best = -1.0
481
- for _ in range(n):
482
- # Generate a random angle between -π/2 and π/2 for the semicircle
483
- ang = unif(-0.5, 0.5) * np.pi
484
- x = self.center + Point(np.cos(ang), np.sin(ang)) * self.radius
485
-
486
- # Check if the sampled point is within the active part of the semicircle
487
- if not self.is_within_boundary(x):
488
- continue
489
-
490
- # Find the minimum distance between the generated point and the provided points
491
- mind = min([x.distance(p) for p in points])
492
- if mind > best:
493
- best = mind
494
- result = x
495
-
496
- return [result]
497
-
498
- def intersect(self, obj: Union[Line, Circle]) -> tuple[Point, ...]:
499
- """Find intersection points with a Line or another Circle, constrained to the semicircle."""
500
- if isinstance(obj, Line):
501
- intersections = obj.intersect(self)
502
- elif isinstance(obj, Circle):
503
- intersections = circle_circle_intersection(self, obj)
504
- else:
505
- return tuple()
506
-
507
- # Filter intersections to only return points within the semicircle
508
- return tuple(p for p in intersections if self.is_within_boundary(p))
509
 
510
 
511
  class HoleCircle(Circle):
@@ -789,7 +792,7 @@ def check_perp(points: list[Point]) -> bool:
789
 
790
  def check_cyclic(points: list[Point]) -> bool:
791
  points = list(set(points))
792
- (a, b, c, *ps) = points
793
  circle = Circle(p1=a, p2=b, p3=c)
794
  for d in ps:
795
  if not close_enough(d.distance(circle.center), circle.radius):
@@ -975,7 +978,7 @@ def draw_angle(
975
 
976
 
977
  def naming_position(
978
- ax: matplotlib.axes.Axes, p: Point, lines: list[Line], circles: list[Circle]
979
  ) -> tuple[float, float]:
980
  """Figure out a good naming position on the drawing."""
981
  _ = ax
@@ -1034,7 +1037,7 @@ def draw_point(
1034
  name = name[0] + '_' + name[1:]
1035
 
1036
  ax.annotate(
1037
- name, naming_position(ax, p, lines, circles), color=color, fontsize=15
1038
  )
1039
 
1040
 
@@ -1106,6 +1109,7 @@ def draw_circle(
1106
  ax: matplotlib.axes.Axes, circle: Circle, color: Any = 'cyan'
1107
  ) -> Circle:
1108
  """Draw a circle."""
 
1109
  if circle.num is not None:
1110
  circle = circle.num
1111
  else:
@@ -1113,11 +1117,14 @@ def draw_circle(
1113
  if len(points) <= 2:
1114
  return
1115
  points = [p.num for p in points]
 
1116
  p1, p2, p3 = points[:3]
1117
  circle = Circle(p1=p1, p2=p2, p3=p3)
1118
-
1119
- _draw_circle(ax, circle, color)
1120
- return circle
 
 
1121
 
1122
  def check_points_semicircle(p1, p2, p3):
1123
  """
@@ -1175,7 +1182,7 @@ def check_points_semicircle(p1, p2, p3):
1175
  }
1176
 
1177
  def _draw_semicircle(
1178
- ax: matplotlib.axes.Axes, P1: Point, P2: Point, P3: Point, color: Any = 'cyan', lw: float = 1.2
1179
  ) -> None:
1180
  """
1181
  Draws a semicircle passing through three points or with one or two points on the diameter.
@@ -1186,45 +1193,71 @@ def _draw_semicircle(
1186
  color (Any): Color of the semicircle.
1187
  lw (float): Line width of the semicircle.
1188
  """
1189
- result = check_points_semicircle((P1.x, P1.y), (P2.x, P2.y), (P3.x, P3.y))
1190
- if not result['is_valid']:
1191
- print("Points are collinear; cannot form a semicircle.")
1192
- return
1193
-
1194
- cx, cy = result['center']
1195
- radius = result['radius']
1196
- diameter_points = result['diameter_points']
1197
-
1198
- # If no pair forms a diameter, determine angles for all three points
1199
- if diameter_points is None:
1200
- # Calculate angles of all three points relative to the circle's center
1201
- angles = np.arctan2(
1202
- [P1.y - cy, P2.y - cy, P3.y - cy],
1203
- [P1.x - cx, P2.x - cx, P3.x - cx]
1204
- )
1205
- angles = (angles + 2 * np.pi) % (2 * np.pi) # Normalize to [0, 2π]
 
 
 
 
 
 
 
 
1206
 
1207
- # Determine the start and end angle for the semicircle
1208
- start_angle = np.min(angles)
1209
- end_angle = np.max(angles)
1210
- if end_angle - start_angle > np.pi:
1211
- start_angle, end_angle = end_angle, start_angle + 2 * np.pi
1212
- else:
1213
- # Use diameter points to define the semicircle angles
1214
- px, py = diameter_points[0]
1215
- qx, qy = diameter_points[1]
1216
- start_angle = np.arctan2(py - cy, px - cx)
1217
- end_angle = np.arctan2(qy - cy, qx - cx)
1218
- if end_angle - start_angle > np.pi:
1219
- start_angle, end_angle = end_angle, start_angle + 2 * np.pi
1220
-
1221
- # Generate points for the semicircle
1222
- t = np.linspace(start_angle, end_angle, 100)
1223
- x = cx + radius * np.cos(t)
1224
- y = cy + radius * np.sin(t)
1225
-
1226
- # Plot the semicircle
1227
- ax.plot(x, y, color=color, lw=lw)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1228
 
1229
  def draw_semicircle(
1230
  ax: matplotlib.axes.Axes, semicircle: SemiCircle, color: Any = 'cyan'
@@ -1296,7 +1329,6 @@ def highlight(
1296
  _draw_line(ax, c, d, color=color2, lw=2.0)
1297
  if name == 'eqangle':
1298
  a, b, c, d, e, f, g, h = args
1299
-
1300
  x = line_line_intersection(Line(a, b), Line(c, d))
1301
  if b.distance(x) > a.distance(x):
1302
  a, b = b, a
@@ -1343,12 +1375,37 @@ def highlight(
1343
  _draw_line(ax, c, d, color=color2, lw=2.0, alpha=0.5)
1344
  _draw_line(ax, m, n, color=color1, lw=2.0, alpha=0.5)
1345
  _draw_line(ax, p, q, color=color2, lw=2.0, alpha=0.5)
1346
- elif name == 'semicircle':
1347
- o, a, b, c = args
1348
- _draw_semicircle(ax, SemiCircle(center=o, p1=a, p2=b, p3=c), color=color1, lw=2.0)
 
 
 
 
 
 
 
 
1349
 
1350
  HCOLORS = None
1351
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1352
 
1353
  def _draw(
1354
  ax: matplotlib.axes.Axes,
@@ -1362,6 +1419,7 @@ def _draw(
1362
  ):
1363
  """Draw everything."""
1364
  colors = ['red', 'green', 'blue', 'orange', 'magenta', 'purple']
 
1365
  pcolor = 'black'
1366
  lcolor = 'black'
1367
  ccolor = 'grey'
@@ -1374,15 +1432,45 @@ def _draw(
1374
  colors = ['grey']
1375
 
1376
  line_boundaries = []
 
 
 
 
1377
  for l in lines:
1378
  p1, p2 = draw_line(ax, l, color=lcolor)
1379
  line_boundaries.append((p1, p2))
 
 
 
 
 
 
 
 
 
 
 
1380
  circles = [draw_circle(ax, c, color=ccolor) for c in circles]
1381
  semicircles = [draw_semicircle(ax, c, color=ccolor) for c in semicircles]
1382
 
1383
  for p in points:
1384
  draw_point(ax, p.num, p.name, line_boundaries, circles, semicircles, color=pcolor)
1385
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1386
  if equals:
1387
  for i, segs in enumerate(equals['segments']):
1388
  color = colors[i % len(colors)]
@@ -1947,6 +2035,9 @@ def sketch_midp(args: tuple[gm.Point, ...]) -> Point:
1947
  a, b = args
1948
  return (a + b) * 0.5
1949
 
 
 
 
1950
 
1951
  def sketch_pentagon(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
1952
  points = [Point(1.0, 0.0)]
 
18
 
19
  import math
20
  from typing import Any, Optional, Union
21
+ import random
22
 
23
  import geometry as gm
24
  import matplotlib
 
27
  import numpy as np
28
  from numpy.random import uniform as unif # pylint: disable=g-importing-member
29
  import graph as gh
30
+ from collections import defaultdict
31
+ from itertools import combinations
32
 
33
+ matplotlib.use('TkAgg')
34
 
35
 
36
  ATOM = 1e-12
 
443
 
444
 
445
  class SemiCircle(Circle):
446
+ """Numerical semicircle, inherits from Circle."""
447
+
448
+ def __init__(
449
+ self,
450
+ center: Optional[Point] = None,
451
+ radius: Optional[float] = None,
452
+ p1: Optional[Point] = None,
453
+ p2: Optional[Point] = None,
454
+ p3: Optional[Point] = None,
455
+ ):
456
+ self.p1 = p1
457
+ self.p2 = p2
458
+ self.p3 = p3
459
+ # Initialize as a Circle
460
+ super().__init__(center, radius, p1, p2, p3)
461
+ # If p1 and p2 define a diameter, set the center and radius accordingly
462
+ if p1 and p2 and not center:
463
+ self.center = Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2)
464
+ self.radius = p1.distance(p2) / 2
465
+ self.r2 = self.radius ** 2
466
+
467
+ # Define the direction or plane for the semicircle (important for sampling and boundaries)
468
+
469
+ def is_within_boundary(self, point: Point) -> bool:
470
+ """Check if a point is within the boundary of the semicircle."""
471
+ vector_to_point = point - self.center
472
+ angle = math.atan2(vector_to_point.y, vector_to_point.x)
473
+
474
+ # Normalize the angle within [0, 2*pi]
475
+ angle = angle if angle >= 0 else (2 * np.pi + angle)
476
+
477
+ # Check if the point is within the semicircle (half of the circle)
478
+ return -np.pi / 2 <= angle <= np.pi / 2
479
+
480
+ def sample_within(self, points: list[Point], n: int = 5) -> list[Point]:
481
+ """Sample a point within the semicircle."""
482
+ result = None
483
+ best = -1.0
484
+ for _ in range(n):
485
+ # Generate a random angle between -π/2 and π/2 for the semicircle
486
+ ang = unif(-0.5, 0.5) * np.pi
487
+ x = self.center + Point(np.cos(ang), np.sin(ang)) * self.radius
488
+
489
+ # Check if the sampled point is within the active part of the semicircle
490
+ if not self.is_within_boundary(x):
491
+ continue
492
+
493
+ # Find the minimum distance between the generated point and the provided points
494
+ mind = min([x.distance(p) for p in points])
495
+ if mind > best:
496
+ best = mind
497
+ result = x
498
+
499
+ return [result]
500
+
501
+ def intersect(self, obj: Union[Line, Circle]) -> tuple[Point, ...]:
502
+ """Find intersection points with a Line or another Circle, constrained to the semicircle."""
503
+ if isinstance(obj, Line):
504
+ intersections = obj.intersect(self)
505
+ elif isinstance(obj, Circle):
506
+ intersections = circle_circle_intersection(self, obj)
507
+ else:
508
+ return tuple()
509
+
510
+ # Filter intersections to only return points within the semicircle
511
+ return tuple(p for p in intersections if self.is_within_boundary(p))
512
 
513
 
514
  class HoleCircle(Circle):
 
792
 
793
  def check_cyclic(points: list[Point]) -> bool:
794
  points = list(set(points))
795
+ (a, b, c, *ps) = points
796
  circle = Circle(p1=a, p2=b, p3=c)
797
  for d in ps:
798
  if not close_enough(d.distance(circle.center), circle.radius):
 
978
 
979
 
980
  def naming_position(
981
+ ax: matplotlib.axes.Axes, p: Point, lines: list[Line], circles: list[Circle], semicircles: list[SemiCircle]
982
  ) -> tuple[float, float]:
983
  """Figure out a good naming position on the drawing."""
984
  _ = ax
 
1037
  name = name[0] + '_' + name[1:]
1038
 
1039
  ax.annotate(
1040
+ name, naming_position(ax, p, lines, circles, semicircles), color=color, fontsize=15
1041
  )
1042
 
1043
 
 
1109
  ax: matplotlib.axes.Axes, circle: Circle, color: Any = 'cyan'
1110
  ) -> Circle:
1111
  """Draw a circle."""
1112
+ name_circle = circle.name
1113
  if circle.num is not None:
1114
  circle = circle.num
1115
  else:
 
1117
  if len(points) <= 2:
1118
  return
1119
  points = [p.num for p in points]
1120
+ print(len(points))
1121
  p1, p2, p3 = points[:3]
1122
  circle = Circle(p1=p1, p2=p2, p3=p3)
1123
+ if "," in name_circle:
1124
+ _draw_circle(ax, circle, color)
1125
+ return circle
1126
+ else:
1127
+ return circle
1128
 
1129
  def check_points_semicircle(p1, p2, p3):
1130
  """
 
1182
  }
1183
 
1184
  def _draw_semicircle(
1185
+ ax: matplotlib.axes.Axes, P1: Point = None, P2: Point = None, P3: Point = None, color: Any = 'cyan', lw: float = 1.2
1186
  ) -> None:
1187
  """
1188
  Draws a semicircle passing through three points or with one or two points on the diameter.
 
1193
  color (Any): Color of the semicircle.
1194
  lw (float): Line width of the semicircle.
1195
  """
1196
+ points = [P for P in (P1, P2, P3) if P is not None] # Filter out None values
1197
+ if len(points) == 2:
1198
+ cx = (P1.x + P2.x) / 2
1199
+ cy = (P1.y + P2.y) / 2
1200
+ radius = np.sqrt((P2.x - P1.x) ** 2 + (P2.y - P1.y) ** 2) / 2
1201
+
1202
+ # Compute angle of the diameter
1203
+ angle_diameter = np.arctan2(P2.y - P1.y, P2.x - P1.x)
1204
+
1205
+ # Randomly determine the orientation of the semicircle
1206
+ offset_angle = np.pi / 2 if random.choice([True, False]) else -np.pi / 2
1207
+
1208
+ # Start and end angles for the semicircle
1209
+ start_angle = angle_diameter + offset_angle
1210
+ end_angle = start_angle + np.pi
1211
+
1212
+ # Generate points for the semicircle
1213
+ t = np.linspace(start_angle, end_angle, 100)
1214
+ x = cx + radius * np.cos(t)
1215
+ y = cy + radius * np.sin(t)
1216
+
1217
+ # Plot the semicircle
1218
+ ax.plot(x, y, color=color, lw=lw)
1219
+
1220
+ if len(points) == 3:
1221
 
1222
+ result = check_points_semicircle((P1.x, P1.y), (P2.x, P2.y), (P3.x, P3.y))
1223
+ if not result['is_valid']:
1224
+ print("Points are collinear; cannot form a semicircle.")
1225
+ return
1226
+
1227
+ cx, cy = result['center']
1228
+ radius = result['radius']
1229
+ diameter_points = result['diameter_points']
1230
+
1231
+ # If no pair forms a diameter, determine angles for all three points
1232
+ if diameter_points is None:
1233
+ # Calculate angles of all three points relative to the circle's center
1234
+ angles = np.arctan2(
1235
+ [P1.y - cy, P2.y - cy, P3.y - cy],
1236
+ [P1.x - cx, P2.x - cx, P3.x - cx]
1237
+ )
1238
+ angles = (angles + 2 * np.pi) % (2 * np.pi) # Normalize to [0, 2π]
1239
+
1240
+ # Determine the start and end angle for the semicircle
1241
+ start_angle = np.min(angles)
1242
+ end_angle = np.max(angles)
1243
+ if end_angle - start_angle > np.pi:
1244
+ start_angle, end_angle = end_angle, start_angle + 2 * np.pi
1245
+ else:
1246
+ # Use diameter points to define the semicircle angles
1247
+ px, py = diameter_points[0]
1248
+ qx, qy = diameter_points[1]
1249
+ start_angle = np.arctan2(py - cy, px - cx)
1250
+ end_angle = np.arctan2(qy - cy, qx - cx)
1251
+ if end_angle - start_angle > np.pi:
1252
+ start_angle, end_angle = end_angle, start_angle + 2 * np.pi
1253
+
1254
+ # Generate points for the semicircle
1255
+ t = np.linspace(start_angle, end_angle, 100)
1256
+ x = cx + radius * np.cos(t)
1257
+ y = cy + radius * np.sin(t)
1258
+
1259
+ # Plot the semicircle
1260
+ ax.plot(x, y, color=color, lw=lw)
1261
 
1262
  def draw_semicircle(
1263
  ax: matplotlib.axes.Axes, semicircle: SemiCircle, color: Any = 'cyan'
 
1329
  _draw_line(ax, c, d, color=color2, lw=2.0)
1330
  if name == 'eqangle':
1331
  a, b, c, d, e, f, g, h = args
 
1332
  x = line_line_intersection(Line(a, b), Line(c, d))
1333
  if b.distance(x) > a.distance(x):
1334
  a, b = b, a
 
1375
  _draw_line(ax, c, d, color=color2, lw=2.0, alpha=0.5)
1376
  _draw_line(ax, m, n, color=color1, lw=2.0, alpha=0.5)
1377
  _draw_line(ax, p, q, color=color2, lw=2.0, alpha=0.5)
1378
+ if name == 'iso_triangle':
1379
+ a, b, c = args
1380
+ _draw_line(ax, c, b, color=color1, lw=2.0)
1381
+ _draw_line(ax, c, a, color=color1, lw=2.0)
1382
+ _draw_line(ax, b, a, color=color2, lw=2.0)
1383
+ if name == 'semicircle':
1384
+ o, a, b, c = args
1385
+ _draw_semicircle(ax, SemiCircle(center=o, p1=a, p2=b, p3=c), color=color1, lw=2.0)
1386
+
1387
+ def convert_point(gm_point: gm.Point) -> Point:
1388
+ return Point(gm_point.num.x, gm_point.num.y)
1389
 
1390
  HCOLORS = None
1391
 
1392
+ def find_pairs_with_same_distance(line_lengths):
1393
+ # Step 1: Group point pairs by distance
1394
+ distance_groups = defaultdict(list)
1395
+ for (p1, p2), distance in line_lengths.items():
1396
+ distance_groups[distance].append((p1, p2))
1397
+
1398
+ # Step 2: Find combinations of pairs with the same distance
1399
+ result = []
1400
+ for pairs in distance_groups.values():
1401
+ if len(pairs) > 1: # Only consider distances with multiple pairs
1402
+ for i in range(len(pairs)):
1403
+ for j in range(i + 1, len(pairs)):
1404
+ line1 = pairs[i]
1405
+ line2 = pairs[j]
1406
+ result.append((line1, line2)) # Group as ((p1, p2), (p3, p4))
1407
+
1408
+ return result
1409
 
1410
  def _draw(
1411
  ax: matplotlib.axes.Axes,
 
1419
  ):
1420
  """Draw everything."""
1421
  colors = ['red', 'green', 'blue', 'orange', 'magenta', 'purple']
1422
+ colors_highlight = [color for color in mcolors.TABLEAU_COLORS.keys() if color not in colors]
1423
  pcolor = 'black'
1424
  lcolor = 'black'
1425
  ccolor = 'grey'
 
1432
  colors = ['grey']
1433
 
1434
  line_boundaries = []
1435
+ line_lengths = {}
1436
+ # Convert all points
1437
+ for p in points:
1438
+ points_numericals = [convert_point(p) for p in points]
1439
  for l in lines:
1440
  p1, p2 = draw_line(ax, l, color=lcolor)
1441
  line_boundaries.append((p1, p2))
1442
+ points_numericals.append(p1)
1443
+ points_numericals.append(p2)
1444
+ unique_points = []
1445
+ seen = set()
1446
+ for p in points_numericals:
1447
+ if (p.x, p.y) not in seen:
1448
+ unique_points.append(p)
1449
+ seen.add((p.x, p.y))
1450
+ print(len(unique_points))
1451
+ for p1, p2 in combinations(unique_points, 2):
1452
+ line_lengths[(p1, p2)] = round(p1.distance(p2), 9)
1453
  circles = [draw_circle(ax, c, color=ccolor) for c in circles]
1454
  semicircles = [draw_semicircle(ax, c, color=ccolor) for c in semicircles]
1455
 
1456
  for p in points:
1457
  draw_point(ax, p.num, p.name, line_boundaries, circles, semicircles, color=pcolor)
1458
 
1459
+ same_length_pairs = find_pairs_with_same_distance(line_lengths)
1460
+ print(len(same_length_pairs))
1461
+
1462
+ length_color_map = {} # Dictionary to map length to its color
1463
+ for i, ((p1, p2), (p3, p4)) in enumerate(same_length_pairs):
1464
+ line_length = p1.distance(p2) # Calculate the length of the line
1465
+ if line_length not in length_color_map: # Check if length is already in the dictionary
1466
+ #color = colors_highlight[i % len(colors_highlight)]
1467
+ color = colors_highlight[i % len(colors_highlight) ] # Assign a new color
1468
+ length_color_map[line_length] = color # Store the length and color in the dictionary
1469
+ else:
1470
+ color = length_color_map[line_length] # Use the existing color for this length
1471
+
1472
+ # Call the highlight function with the determined color
1473
+ highlight(ax, 'cong', [p1, p2, p3, p4], lcolor, color, color)
1474
  if equals:
1475
  for i, segs in enumerate(equals['segments']):
1476
  color = colors[i % len(colors)]
 
2035
  a, b = args
2036
  return (a + b) * 0.5
2037
 
2038
+ def sketch_foot(args: tuple[gm.Point, ...]) -> Point:
2039
+ a, b, c = args
2040
+ return a.foot(Line(b, c))
2041
 
2042
  def sketch_pentagon(args: tuple[gm.Point, ...]) -> tuple[Point, ...]:
2043
  points = [Point(1.0, 0.0)]