refactor image rendering script

This commit is contained in:
queue-miscreant 2025-03-10 05:01:31 -05:00
parent d9337616db
commit 6ddfa44cf9

View File

@ -1,389 +1,367 @@
import itertools
from dataclasses import dataclass
from typing import Iterable
import numpy as np
from numpy.typing import NDArray
import matplotlib.pyplot as plt
root3 = 0.5 - (3**0.5) / 2j
triangle_coords = lambda i: (i[0] + i[1] * root3.real, i[1] * root3.imag)
embed_triangular = lambda i: (i[0] + i[1] * root3.real, i[1] * root3.imag)
def hex_positions(n):
def plot_triangular(plottype="plot"):
"""
Plot an iterable of coordinates in a triangular grid using the member `plottype` of `pyplot`.
Coordinates are in the form `(x, y)`.
Extra arguments are passed to the plotting method.
"""
def ret(coords: Iterable[tuple[int, int] | NDArray], *args, **kwargs):
getattr(plt, plottype)(
*list(zip(*map(embed_triangular, coords))), *args, **kwargs
)
return ret
def hex_positions(n: int, offset: int = 0) -> list[tuple[int, int]]:
"""
List of coordinates which, in a triangular lattice, form the vertices of hexagons.
The points (1, 1), (3, 0), and (0, 3) are all at the center of hexagons,
so linear combinations of these are NOT part of the grid.
"""
coordinates = [
(i, j) for i in range(n) for j in range(n) if i + j < n and (i - j) % 3
(i + offset, j + offset)
for i in range(n)
for j in range(n)
if i + j < n and (i - j) % 3 != 0
]
drawing = [(i + j * root3.real, j * root3.imag) for (i, j) in coordinates]
return list(zip(*drawing))
return coordinates
def hex_lines(n):
def draw_hex_lines(n: int, offset: int = 0):
# vertices of hexagons in rightmost third of the the upper half plane
coordinates = [
(i, j) for i in range(n) for j in range(n) if i + j < n and (i - j) % 3
]
hex_coordinates = hex_positions(n, offset)
# find connections between a vertex and its (up to 3) neighbors
lines = [
filter(
lambda x: x[1] in coordinates,
[[(i, j), (i + 1, j)], [(i, j), (i, j + 1)], [(i, j), (i + 1, j - 1)]],
list(
itertools.chain.from_iterable(
zip(
# the point being considered
itertools.repeat((i, j)),
# its neighbors which exist in the grid
filter(
lambda x: x in hex_coordinates,
[
(i + 1, j),
(i, j + 1),
(i + 1, j - 1),
],
),
)
)
)
for (i, j) in coordinates
for i, j in hex_coordinates
]
for line in lines:
for unfiltered in line:
plt.plot(*list(zip(*map(triangle_coords, unfiltered))), "ko-")
# flat_drawing = [map(triangle_coords, unzip) for line in lines for unzip in line]
# return list(zip(*flat_drawing))
# for i in flat_drawing:
# plt.plot(*list(zip(*i)), "k")
# draw lines between connected points
plot_triangular("plot")(line, "ko-")
def triangular_grid(n, show_hexes=False):
coordinates = [(i, j) for i in range(n) for j in range(n) if i + j < n]
drawing = [(i + j * root3.real, j * root3.imag) for (i, j) in coordinates]
# plt.plot(*list(zip(*drawing)), "o")
plt.triplot(*list(zip(*drawing)), "-", color="0.7", lw=0.75)
def draw_triangular_grid(n: int, offset: int = 0, show_hexes: bool = False):
plot_triangular("triplot")(
[(i + offset, j + offset) for i in range(n) for j in range(n) if i + j < n],
"-",
color="0.7",
lw=0.75,
)
if show_hexes:
# plt.plot(*hex_positions(n), "ok")
hex_lines(n)
draw_hex_lines(n, offset=offset)
plt.gca().axis("off")
plt.xlim((-2, (1920 / 1080) * plt.ylim()[1]))
"""
def color_at_hex_coordinates(coord):
draw = None
if min(coord) == 0: #class 1
#(0,1) = (1,0) => (2,1), (1,2), (2, 2)
#(0,2) = (2,0) => (3,2), (2,3), (3, 3)
big = max(coord)
draw = [(big, big+1), (big+1, big), (big+1, big+1)]
#plt.fill([big, big+1, big+1], [big, big, big+1],"r")
elif (coord[0] == coord[1]): #class 2
#(1,1) => (2,0) |+| (1,1)
#(2,2) => (3,1) |+| (1,1)
#(n,n) => (n+1,n-1) |+| (1,1)
big = coord[0]
draw = [(big+1, big-1), (big+1, big), (big+2, big-1)]
#plt.fill([big+1, big+2, big+1], [big-1, big-1, big],"r")
else:
#choose one enantiomer
#(2,1) => [(3, 1), (3, 2), (4, 1)]
#(3,1) => [(4, 2), (3, 2), (4, 1)]
#(a,b) => [(a+1, b), (a+1, b+1), (a+2, b)]
a,b = coord
draw = [(a+1, b), (a+1, b+1), (a+2, b)]
class Triangle:
def __init__(
self,
left: tuple[int, int] | NDArray,
right: tuple[int, int] | NDArray,
top: tuple[int, int] | NDArray,
):
self.left = np.array(left)
self.right = np.array(right)
self.top = np.array(top)
plt.fill(*list(zip(*map(triangle_coords, draw))), "r")
"""
def __iter__(self):
yield self.left
yield self.right
yield self.top
yield self.left
def identify_triangles(coord, offset=0, point=True, fill_all=False, path=False):
# (a,b) specifies coordinates to the center of a hexagon
# (a,a) is the first hexagon for turning
# (2b, -b) is the length of the path after the 60 degree turn, meaning
# (a+2b, a-b) is the center of the hexagon
a, b = coord
if a != b and min(coord) > 0 and offset == 0:
offset = a - b
# origin
plt.fill(
*list(
zip(
*map(
triangle_coords,
[(offset, offset), (offset + 1, offset), (offset, offset + 1)],
)
)
),
"r",
def draw_class_i(a: int, fill_all: bool = False):
"""
Plot a Class I Goldberg-Coxeter sector with parameter (a, 0)
"""
origin = Triangle(
left=(0, 0),
right=(1, 0),
top=(0, 1),
)
plot_triangular("fill")(origin, "r")
terminal = Triangle(
right=(a, a),
left=(a - 1, a),
top=(a, a - 1), # actually the bottom
)
plot_triangular("fill")(terminal, "b")
bottom_sector = Triangle(
left=origin.left,
right=(a, 0),
top=(0, a),
)
top_sector = Triangle(
left=(0, a),
right=terminal.right,
top=(a, 0),
)
# control point
if point:
plt.plot(*triangle_coords((a + offset, b + offset)), "bo")
# outline sector
plot_triangular("plot")(
[
origin.top,
origin.right,
bottom_sector.right,
terminal.top,
terminal.left,
bottom_sector.top,
origin.top,
],
color="0.25",
lw=2,
)
c, d = (a - offset + 2 * (b + offset), a - b + offset)
if min(coord) == 0:
# upside-down triangle
triangle = [(c, d), (c - 1, d), (c, d - 1)]
# terminal
plt.fill(*list(zip(*map(triangle_coords, triangle))), "b")
if fill_all:
plot_triangular("fill")(bottom_sector, "r")
plot_triangular("fill")(top_sector, "b")
if fill_all:
plt.fill(
*list(
zip(
*map(
triangle_coords,
[
(offset, offset),
(offset + a, offset),
(offset, offset + a),
],
)
)
),
"r",
)
plt.fill(
*list(
zip(
*map(
triangle_coords,
[
(offset, offset + a),
(offset + a, offset),
(offset + a, offset + a),
],
)
)
),
"b",
)
else:
triangle = [(c, d), (c - 1, d), (c - 1, d + 1)]
# terminal
plt.fill(*list(zip(*map(triangle_coords, triangle))), "b")
# alternate terminal
c, d = (b - a + offset, b - offset + 2 * (a + offset))
triangle2 = [(c, d), (c, d - 1), (c + 1, d - 1)]
plt.fill(*list(zip(*map(triangle_coords, triangle2))), "g")
# lines connecting triangles
plt.plot(
*list(
zip(
*map(
triangle_coords,
[
(offset + 1, offset),
triangle[1],
triangle[2],
triangle2[2],
triangle2[1],
(offset, offset + 1),
(offset + 1, offset),
],
)
)
),
color="0.25",
lw=2,
)
def draw_trapezoids(a: int):
"""Draw trapezoidal regions in a Class II Goldberg-Coxeter sector"""
plot_triangular("fill")(
[(0, 0), (0, a), (a, a), (2 * a, 0)],
"r",
)
plot_triangular("fill")(
[(0, a), (a, a), (a, 2 * a), (0, 3 * a)],
"g",
)
plot_triangular("fill")(
[(a, a), (a, 2 * a), (3 * a, 0), (2 * a, 0)],
"b",
)
if fill_all and a == b:
# trapezoids
plt.fill(
*list(
zip(
*map(
triangle_coords,
[
(offset, offset),
(offset, offset + a),
(offset + a, offset + a),
(offset + 2 * a, offset),
],
)
)
),
"r",
)
plt.fill(
*list(
zip(
*map(
triangle_coords,
[
(offset, offset + a),
(offset + a, offset + a),
(offset + a, offset + 2 * a),
(offset, offset + 3 * a),
],
)
)
),
"g",
)
plt.fill(
*list(
zip(
*map(
triangle_coords,
[
(offset + a, offset + a),
(offset + a, offset + 2 * a),
(offset + 3 * a, offset),
(offset + 2 * a, offset),
],
)
)
),
"b",
)
elif fill_all == 1:
plt.fill(
*list(
zip(
*map(
triangle_coords,
[
(offset + 1, offset),
triangle[1],
triangle[2],
triangle2[2],
triangle2[1],
(offset, offset + 1),
(offset + 1, offset),
],
)
)
),
color="0.4",
)
def draw_classes_ii_iii(parameter: tuple[int, int], fill_all: int):
"""
Plot a Class II or III Goldberg-Coxeter sector with parameter (a, b)
elif fill_all == 2:
# dark gray interior
plt.fill(
*list(
zip(
*map(
triangle_coords,
[
(offset + 1, offset),
triangle[1],
triangle[2],
triangle2[2],
triangle2[1],
(offset, offset + 1),
(offset + 1, offset),
],
)
)
),
Parameters where the second term is larger are NOT plotted correctly by fill_all.
"""
# (a, b) specifies coordinates to the center of a hexagon
a, b = parameter
# (a, a) is the hexagon we turn at,
# (2b, -b) is the length of the path after the 60 degree turn, so
# (a+2b, a-b) is the center of the hexagon
w = a + 2 * b - 1
l = a - b
# origin
origin = Triangle(
left=(0, 0),
right=(1, 0),
top=(0, 1),
)
plot_triangular("fill")(origin, "r")
# terminal triangle #1
terminal1 = Triangle(right=(w + 1, l), left=(w, l), top=(w, l + 1))
plot_triangular("fill")(terminal1, "b")
# terminal triangle #2
terminal2 = Triangle(top=(-l, w + l + 1), left=(-l, w + l), right=(-l + 1, w + l))
plot_triangular("fill")(terminal2, "g")
# lines connecting triangles
plot_triangular("plot")(
[
origin.top,
origin.right,
terminal1.left,
terminal1.top,
terminal2.right,
terminal2.left,
origin.top,
],
color="0.25",
lw=2,
)
if fill_all:
if a == b:
draw_trapezoids(a)
else:
plot_triangular("fill")(
[
origin.top,
origin.right,
terminal1.left,
terminal1.top,
terminal2.right,
terminal2.left,
origin.top,
],
color="0.4",
)
# lower parallelogram
plt.fill(
*list(
zip(
*map(
triangle_coords,
[
(offset + 1, offset),
(offset + 1, offset + offset),
triangle[1],
(lambda x, y: (x, y - offset))(*triangle[1]),
(offset + 1, offset),
],
)
)
),
plot_triangular("fill")(
[
origin.right,
(1, l),
terminal1.left,
terminal1.left + (0, -l),
origin.right,
],
color="xkcd:light red",
)
# upper right parallelogram
plt.fill(
*list(
zip(
*map(
triangle_coords,
[
triangle[2],
(lambda x, y: (x - offset, y))(*triangle[2]),
triangle2[2],
(lambda x, y: (x + offset, y))(*triangle2[2]),
triangle[2],
],
)
)
),
plot_triangular("fill")(
[
terminal1.top,
terminal1.top + (-l, 0),
terminal2.right,
terminal2.right + (l, 0),
terminal1.top,
],
color="xkcd:light blue",
)
# upper left parallelogram
plt.fill(
*list(
zip(
*map(
triangle_coords,
[
triangle2[1],
(lambda x, y: (x + offset, y - offset))(*triangle2[1]),
(offset, offset + 1),
(0, offset + 1 + offset),
triangle2[1],
],
)
)
),
plot_triangular("fill")(
[
terminal2.left,
terminal2.left + (l, -l),
origin.top,
origin.top + (-l, l),
terminal2.left,
],
color="xkcd:light green",
)
# center triangle
plt.fill(
*list(
zip(
*map(
triangle_coords,
[
(offset, offset + offset),
(lambda x, y: (x - offset + 1, y - 1))(*triangle[2]),
(lambda x, y: (x + offset, y - offset + 1))(
*triangle2[1]
),
(offset, offset + offset),
],
)
)
),
plot_triangular("fill")(
[
(0, a - b),
(3 * b, a - b),
(0, a + 2 * b),
(0, a - b),
],
color="0.8",
)
def draw_path(a: int, b: int):
plot_triangular("plot")(
[(i, i) for i in range(a + 1)],
marker="o",
color="xkcd:light red",
mfc="k",
lw=3,
)
plt.text(
*embed_triangular((a / 2 - 1, a / 2 + 1.5)),
f"a = {a}",
bbox={
"facecolor": "xkcd:light red",
"pad": 6,
},
)
plot_triangular("plot")(
[(a + 2 * i, a - i) for i in range(b + 1)],
marker="o",
color="xkcd:light blue",
mfc="k",
lw=3,
)
plt.text(
*embed_triangular((a + b, a - b / 2 + 0.75)),
f"b = {b}",
bbox={
"facecolor": "xkcd:light blue",
"pad": 6,
},
)
def recenter_axes(a: int, b: int):
minx, maxx = plt.xlim()
miny, maxy = plt.ylim()
xspan = maxx - minx
yspan = maxy - miny
extent_x = a + 2*b + (a - b) * root3.real # rightward extent due to blue triangle
if a > b and min(a, b) != 0:
extent_x += -(a-b) + (1 + a-b) * root3.real # leftward extent due to green parallelogram
extent_y = (2 * max(a, b) + min(a, b)) * root3.imag
if min(a, b) == 0 or (extent_x * yspan / xspan) > extent_y:
span_diff = extent_x * yspan / xspan - extent_y
plt.xlim(-0.1, extent_x + 0.1)
plt.ylim(span_diff / 2, (extent_x + 0.2) * yspan / xspan + span_diff / 2)
else:
span_diff = extent_y * xspan / yspan - extent_x
# the figure extends below the y axis if a < b
if a >= b:
plt.ylim(-0.1, extent_y + 0.1)
else:
plt.ylim(
-extent_y / (2 * a + 1) - 0.1, extent_y * (1 - (1 / (2 * a + 1))) + 0.1
)
plt.xlim(-span_diff / 2, (extent_y + 0.2) * xspan / yspan - span_diff / 2)
def draw_sector(parameter: tuple[int, int], fill_all: bool = False, path: bool = False):
"""
Draw a sector from the Goldberg-Coxeter construction with a certain `parameter`.
`offset` will translate the figure to the point `(offset, offset)` in a triangular plane.
`fill_all` controls whether the sector should be filled.
`path` controls whether to label the path described by `parameter`
"""
draw_triangular_grid(50, offset=-10, show_hexes=True)
# (a, b) specifies coordinates to the center of a hexagon
a, b = parameter
if min(parameter) == 0:
draw_class_i(max(parameter), fill_all)
else:
draw_classes_ii_iii(parameter, fill_all)
if path:
plt.plot(
*list(
zip(
*map(
lambda x: triangle_coords((offset + x, offset + x)),
range(a + 1),
)
)
),
marker="o",
color="xkcd:light red",
mfc="k",
lw=3,
)
plt.text(
*triangle_coords((offset + a / 2 - 1, offset + a / 2 + 1.5)),
f"a = {a}",
bbox={"facecolor": "xkcd:light red", "pad": 6},
)
draw_path(*parameter)
plt.plot(
*list(
zip(
*map(
lambda x: triangle_coords((offset + a + 2 * x, offset + a - x)),
range(b + 1),
)
)
),
marker="o",
color="xkcd:light blue",
mfc="k",
lw=3,
)
plt.text(
*triangle_coords((offset + a + b, offset + a - b / 2 + 0.75)),
f"b = {b}",
bbox={"facecolor": "xkcd:light blue", "pad": 6},
)
recenter_axes(*parameter)
if __name__ == "__main__":
triangular_grid(24, True)
# identify_triangles((4,2), point=False, path=True)
identify_triangles((4, 2), offset=5, point=False, path=True)
plt.show()
# if __name__ == "__main__":
# draw_triangular_grid(24, True)
# # draw_sector((4,2), point=False, path=True)
# draw_sector((4, 2), offset=5, point=False, path=True)
# plt.show()