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 import numpy as np
from numpy.typing import NDArray
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
root3 = 0.5 - (3**0.5) / 2j 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 = [ 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 coordinates
return list(zip(*drawing))
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 # vertices of hexagons in rightmost third of the the upper half plane
coordinates = [ hex_coordinates = hex_positions(n, offset)
(i, j) for i in range(n) for j in range(n) if i + j < n and (i - j) % 3
]
# find connections between a vertex and its (up to 3) neighbors # find connections between a vertex and its (up to 3) neighbors
lines = [ lines = [
filter( list(
lambda x: x[1] in coordinates, itertools.chain.from_iterable(
[[(i, j), (i + 1, j)], [(i, j), (i, j + 1)], [(i, j), (i + 1, j - 1)]], 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 line in lines:
for unfiltered in line: # draw lines between connected points
plt.plot(*list(zip(*map(triangle_coords, unfiltered))), "ko-") plot_triangular("plot")(line, "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")
def triangular_grid(n, show_hexes=False): def draw_triangular_grid(n: int, offset: int = 0, show_hexes: bool = False):
coordinates = [(i, j) for i in range(n) for j in range(n) if i + j < n] plot_triangular("triplot")(
drawing = [(i + j * root3.real, j * root3.imag) for (i, j) in coordinates] [(i + offset, j + offset) for i in range(n) for j in range(n) if i + j < n],
# plt.plot(*list(zip(*drawing)), "o") "-",
plt.triplot(*list(zip(*drawing)), "-", color="0.7", lw=0.75) color="0.7",
lw=0.75,
)
if show_hexes: if show_hexes:
# plt.plot(*hex_positions(n), "ok") draw_hex_lines(n, offset=offset)
hex_lines(n)
plt.gca().axis("off") plt.gca().axis("off")
plt.xlim((-2, (1920 / 1080) * plt.ylim()[1]))
""" class Triangle:
def color_at_hex_coordinates(coord): def __init__(
draw = None self,
if min(coord) == 0: #class 1 left: tuple[int, int] | NDArray,
#(0,1) = (1,0) => (2,1), (1,2), (2, 2) right: tuple[int, int] | NDArray,
#(0,2) = (2,0) => (3,2), (2,3), (3, 3) top: tuple[int, int] | NDArray,
big = max(coord) ):
draw = [(big, big+1), (big+1, big), (big+1, big+1)] self.left = np.array(left)
#plt.fill([big, big+1, big+1], [big, big, big+1],"r") self.right = np.array(right)
elif (coord[0] == coord[1]): #class 2 self.top = np.array(top)
#(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)]
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): def draw_class_i(a: int, fill_all: bool = False):
# (a,b) specifies coordinates to the center of a hexagon """
# (a,a) is the first hexagon for turning Plot a Class I Goldberg-Coxeter sector with parameter (a, 0)
# (2b, -b) is the length of the path after the 60 degree turn, meaning """
# (a+2b, a-b) is the center of the hexagon origin = Triangle(
a, b = coord left=(0, 0),
if a != b and min(coord) > 0 and offset == 0: right=(1, 0),
offset = a - b top=(0, 1),
# origin )
plt.fill( plot_triangular("fill")(origin, "r")
*list(
zip( terminal = Triangle(
*map( right=(a, a),
triangle_coords, left=(a - 1, a),
[(offset, offset), (offset + 1, offset), (offset, offset + 1)], top=(a, a - 1), # actually the bottom
) )
) plot_triangular("fill")(terminal, "b")
),
"r", 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 # outline sector
if point: plot_triangular("plot")(
plt.plot(*triangle_coords((a + offset, b + offset)), "bo") [
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 fill_all:
if min(coord) == 0: plot_triangular("fill")(bottom_sector, "r")
# upside-down triangle plot_triangular("fill")(top_sector, "b")
triangle = [(c, d), (c - 1, d), (c, d - 1)]
# terminal
plt.fill(*list(zip(*map(triangle_coords, triangle))), "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 def draw_trapezoids(a: int):
plt.plot( """Draw trapezoidal regions in a Class II Goldberg-Coxeter sector"""
*list( plot_triangular("fill")(
zip( [(0, 0), (0, a), (a, a), (2 * a, 0)],
*map( "r",
triangle_coords, )
[ plot_triangular("fill")(
(offset + 1, offset), [(0, a), (a, a), (a, 2 * a), (0, 3 * a)],
triangle[1], "g",
triangle[2], )
triangle2[2], plot_triangular("fill")(
triangle2[1], [(a, a), (a, 2 * a), (3 * a, 0), (2 * a, 0)],
(offset, offset + 1), "b",
(offset + 1, offset), )
],
)
)
),
color="0.25",
lw=2,
)
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: def draw_classes_ii_iii(parameter: tuple[int, int], fill_all: int):
plt.fill( """
*list( Plot a Class II or III Goldberg-Coxeter sector with parameter (a, b)
zip(
*map(
triangle_coords,
[
(offset + 1, offset),
triangle[1],
triangle[2],
triangle2[2],
triangle2[1],
(offset, offset + 1),
(offset + 1, offset),
],
)
)
),
color="0.4",
)
elif fill_all == 2: Parameters where the second term is larger are NOT plotted correctly by fill_all.
# dark gray interior """
plt.fill( # (a, b) specifies coordinates to the center of a hexagon
*list( a, b = parameter
zip( # (a, a) is the hexagon we turn at,
*map( # (2b, -b) is the length of the path after the 60 degree turn, so
triangle_coords, # (a+2b, a-b) is the center of the hexagon
[ w = a + 2 * b - 1
(offset + 1, offset), l = a - b
triangle[1],
triangle[2], # origin
triangle2[2], origin = Triangle(
triangle2[1], left=(0, 0),
(offset, offset + 1), right=(1, 0),
(offset + 1, offset), 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", color="0.4",
) )
# lower parallelogram # lower parallelogram
plt.fill( plot_triangular("fill")(
*list( [
zip( origin.right,
*map( (1, l),
triangle_coords, terminal1.left,
[ terminal1.left + (0, -l),
(offset + 1, offset), origin.right,
(offset + 1, offset + offset), ],
triangle[1],
(lambda x, y: (x, y - offset))(*triangle[1]),
(offset + 1, offset),
],
)
)
),
color="xkcd:light red", color="xkcd:light red",
) )
# upper right parallelogram # upper right parallelogram
plt.fill( plot_triangular("fill")(
*list( [
zip( terminal1.top,
*map( terminal1.top + (-l, 0),
triangle_coords, terminal2.right,
[ terminal2.right + (l, 0),
triangle[2], terminal1.top,
(lambda x, y: (x - offset, y))(*triangle[2]), ],
triangle2[2],
(lambda x, y: (x + offset, y))(*triangle2[2]),
triangle[2],
],
)
)
),
color="xkcd:light blue", color="xkcd:light blue",
) )
# upper left parallelogram # upper left parallelogram
plt.fill( plot_triangular("fill")(
*list( [
zip( terminal2.left,
*map( terminal2.left + (l, -l),
triangle_coords, origin.top,
[ origin.top + (-l, l),
triangle2[1], terminal2.left,
(lambda x, y: (x + offset, y - offset))(*triangle2[1]), ],
(offset, offset + 1),
(0, offset + 1 + offset),
triangle2[1],
],
)
)
),
color="xkcd:light green", color="xkcd:light green",
) )
# center triangle # center triangle
plt.fill( plot_triangular("fill")(
*list( [
zip( (0, a - b),
*map( (3 * b, a - b),
triangle_coords, (0, a + 2 * b),
[ (0, a - b),
(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),
],
)
)
),
color="0.8", 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: if path:
plt.plot( draw_path(*parameter)
*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},
)
plt.plot( recenter_axes(*parameter)
*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},
)
if __name__ == "__main__": # if __name__ == "__main__":
triangular_grid(24, True) # draw_triangular_grid(24, True)
# identify_triangles((4,2), point=False, path=True) # # draw_sector((4,2), point=False, path=True)
identify_triangles((4, 2), offset=5, point=False, path=True) # draw_sector((4, 2), offset=5, point=False, path=True)
plt.show() # plt.show()