cut.py
6.3 kB · python · 204 lines
1import math2import re3import sys45import lib # noqa: F40167from PIL import Image8from mrlypy.core import binary, formulas9from mrlypy.core.colors import alpha10from mrlypy.six import FILL, GRID, VOID11from mrlypy.six.designs import carpet_cut12from mrlypy.six.renderer import draw, svg13from mrlypy.two.renderer import svg_square, to_image1415from lib.canvas import H3, INK, PAPER, flatten, quantize16from lib.gif import write_gif17from lib.paths import CUTS, GIFS, GRIDS, ensure, show, write18from lib.terminal import menu, pick_level, pick_number1920SIZE = 108021SUPER = 322GIFW = 81023GIFH = round(GIFW * H3)24GIFSQ = GIFW * 2 // 325GREYS = 1626NUMBERS = [1, 3, 5, 7, 9]27DEPTH = 228LEVELS = {1: 3, 3: 3, 5: 3}29SVG_CAP = 12000030CUBE_CAP = 4 * 1024 ** 331GLYPHS = {VOID: "0", FILL: "1", GRID: "-"}32PALETTE = {VOID: [PAPER], FILL: [INK], GRID: [alpha]}3334# CUT3536def depth(number):37 return LEVELS.get(number, DEPTH)3839def cost(number, level):40 return (4 * number ** level) ** 34142def cut(number, level):43 weight = cost(number, level)44 if weight > CUBE_CAP:45 sys.exit("%d-%d needs %.1f GB: mrlypy.six.cut blows the cube up by 4 before slicing it"46 % (number, level, weight / 1e9))47 return carpet_cut(number, level).paint(PALETTE)4849# CANVAS5051def save_png(path, image):52 image.convert("L").save(path, optimize=True)5354# TRIANGLES5556def triangles(cell, width):57 scale = max(1, math.ceil(SUPER * width / (cell.width + 1)))58 image = flatten(draw(cell, scale=scale, start=cell.start))59 height = max(1, round(width * image.height * H3 / image.width))60 return image.resize((width, height), Image.BOX)6162def png_tri(path, cell):63 save_png(path, triangles(cell, SIZE))6465def equilateral(markup):66 lines = markup.split("\n")67 if len(lines) < 3:68 return markup69 head, body = lines[0], lines[1:-1]70 width = int(re.search(r'width="(\d+)"', head).group(1))71 height = int(re.search(r'height="(\d+)"', head).group(1)) * H372 box = '<svg width="%.3f" height="%.3f" viewBox="0 0 %.3f %.3f" xmlns="http://www.w3.org/2000/svg">'73 return "\n".join(74 [box % (width, height, width, height), '<g transform="scale(1,%.7f)">' % H3]75 + body76 + ["</g>", "</svg>"]77 )7879def svg_tri(path, cell):80 write(path, equilateral(svg(cell, scale=1, start=cell.start)))8182# SQUARES8384def png_sq(path, cell):85 image = flatten(to_image(cell.cell))86 height = max(1, round(SIZE * image.height / image.width))87 save_png(path, image.resize((SIZE, height), Image.NEAREST))8889def svg_sq(path, cell):90 write(path, svg_square(cell.cell))9192# GIF9394def frame(cell):95 image = triangles(cell, GIFW).convert("L")96 image.thumbnail((GIFW, GIFH), Image.BOX)97 canvas = Image.new("L", (GIFW, GIFH), PAPER.r)98 canvas.paste(image, ((GIFW - image.width) // 2, (GIFH - image.height) // 2))99 return quantize(canvas, GREYS)100101def frame_sq(cell):102 image = flatten(to_image(cell.cell)).convert("L")103 height = max(1, round(GIFW * image.height / image.width))104 image = image.resize((GIFW, height), Image.NEAREST)105 canvas = Image.new("L", (GIFW, GIFSQ), PAPER.r)106 canvas.paste(image, (0, (GIFSQ - height) // 2))107 return quantize(canvas, GREYS)108109# TEXT110111def write_txt(path, cell):112 write(path, "\n".join(cell.text(GLYPHS)))113114def preview(cell):115 rows = cell.text({VOID: " ", FILL: "█", GRID: " "})116 step = max(1, round(cell.height / 31))117 stride = max(1, round(step * 2 * H3))118 for row in rows[::step]:119 print(row[::stride].rstrip())120121# RUN122123def stats(number, level, cell):124 fills = int((cell.types == FILL).sum())125 print("sponge %d level %d: %d triangles in the cut" % (number, level, fills))126 print("closed form says %d" % formulas.carpet_fill_triangles(number, level))127 if number >= 3:128 seed = int(binary.carpet_3d(number).sum())129 print("dimension log(%d)/log(%d) = %.4f"130 % (seed, number, formulas.carpet_3d_dimension(number)))131132def one(number, level, cell):133 stem = "cut-%d-%d" % (number, level)134 grid = "grid-%d-%d" % (number, level)135 write_txt(CUTS / (stem + ".txt"), cell)136 png_tri(CUTS / (stem + ".png"), cell)137 png_sq(GRIDS / (grid + ".png"), cell)138 count = cell.width * cell.height139 if count <= SVG_CAP:140 svg_tri(CUTS / (stem + ".svg"), cell)141 svg_sq(GRIDS / (grid + ".svg"), cell)142 else:143 print("skipped svg for %s: %d cells" % (stem, count))144145def draw_one(number, level):146 ensure()147 cell = cut(number, level)148 preview(cell)149 one(number, level, cell)150 stats(number, level, cell)151 return 0152153def sweep():154 ensure()155 cells = {}156 for number in NUMBERS:157 for level in range(1, depth(number) + 1):158 cells[(number, level)] = cut(number, level)159 print("grid %d-%d ready" % (number, level))160 frames = {}161 squares = {}162 for (number, level), cell in sorted(cells.items()):163 one(number, level, cell)164 frames[(number, level)] = frame(cell)165 squares[(number, level)] = frame_sq(cell)166 print("drew %d-%d" % (number, level))167 for number in NUMBERS:168 story = [frames[(number, l)] for l in range(1, depth(number) + 1)]169 if number != 1:170 story.insert(0, frames[(1, 1)]) # level 0: the solid cube171 write_gif(GIFS / ("cut-levels-%d.gif" % number), story)172 deepest = max(depth(n) for n in NUMBERS)173 for level in range(1, deepest + 1):174 write_gif(GIFS / ("cut-numbers-%d.gif" % level),175 [frames[(n, level)] for n in NUMBERS if depth(n) >= level])176 write_gif(GIFS / ("grid-numbers-%d.gif" % level),177 [squares[(n, level)] for n in NUMBERS if depth(n) >= level])178 print("wrote %d gifs in %s" % (len(NUMBERS) + 2 * deepest, show(GIFS)))179 return 0180181# TERMINAL182183COMMANDS = {184 "sweep": (sweep, "every number at its level, then the gifs"),185 "draw": (draw_one, "draw one NUMBER LEVEL"),186}187188def help():189 menu("cut.py <command> <number> <level> slice a generalized Menger sponge",190 COMMANDS,191 ["%d to level %d" % (number, depth(number)) for number in NUMBERS],192 "'sweep' alone draws the lot; 'draw' takes any odd number and any level.")193194def terminal():195 match sys.argv[1:]:196 case ["sweep"]:197 sys.exit(sweep() or 0)198 case ["draw", number, level]:199 sys.exit(draw_one(pick_number(number), pick_level(level)) or 0)200 case _:201 help()202203if __name__ == "__main__":204 terminal()