Last active
May 26, 2021 05:29
-
-
Save neizod/506e32d7841b27399e67e8826445b4df to your computer and use it in GitHub Desktop.
Megagon SVG!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# NOTE | |
# LAYERS is a non-empty list of integers. | |
# The n-gon can be determine by calc_ngon(LAYERS), e.g., | |
#LAYERS = [1, 2, 2] # heptagon (not constructible) | |
#LAYERS = [1, 4, 3] # heptadecagon (17 sides) | |
#LAYERS = [2, 2, 3, 2] # tetracontadigon (42) | |
#LAYERS = [4, 1, 4, 3, 2, 2] # 360-gon | |
#LAYERS = [4, 3, 2, 4, 3, 2] # chiliagon (1,000) | |
#LAYERS = [1, 9, 10, 10, 10] # myriagon (10,000) | |
#LAYERS = [1, 16, 15, 16, 16] # 65537-gon | |
LAYERS = [100, 99, 100] # megagon (1,000,000) | |
# NOTE | |
# Otherwise, just use NGON to describe the polygon, | |
# and let the program determine a pattern for LAYERS automatically. | |
#NGON = 17 | |
PADDING = 50 | |
RADIUS = 700 | |
PRECISION = 9 | |
############################################################################## | |
import sys | |
from types import SimpleNamespace as namespace | |
from math import sin, cos, tan, radians | |
from operator import mul | |
from functools import reduce | |
from itertools import product | |
from collections import Counter | |
def translate(center, x, y): | |
return x+center, y+center | |
def _rotate(angle, x, y): | |
return x*cos(angle) - y*sin(angle), x*sin(angle) + y*cos(angle) | |
def rotate(x, y, angle, center): | |
return translate(center, *_rotate(angle, *translate(-center, x, y))) | |
def iter_primes(memo=namespace(pi=0, ps=[2, 3])): | |
k = 0 | |
while True: | |
while len(memo.ps) <= k: | |
head = memo.ps[memo.pi]**2 + 1 | |
memo.pi += 1 | |
tail = memo.ps[memo.pi]**2 | |
seive = list(range(head, tail)) | |
for p in memo.ps[:memo.pi]: | |
size = 1 + (tail - head + (head % -p)) // p | |
seive[-head%p::p] = [0] * size | |
memo.ps += (p for p in seive if p) | |
yield memo.ps[k] | |
k += 1 | |
def factors(n): | |
if n == 1: | |
return [1] | |
fs = [] | |
for p in iter_primes(): | |
if p**2 > n: | |
break | |
while n % p == 0: | |
fs += [p] | |
n //= p | |
if n > 1: | |
fs += [n] | |
return fs | |
def divisors(n): | |
gfs = ({p**i for i in range(k+1)} for p, k in Counter(factors(n)).items()) | |
return sorted(reduce(mul, ts) for ts in product(*gfs)) | |
def calc_ngon(layers): | |
r = 1 | |
for v in reversed(layers): | |
r *= v | |
r += 1 | |
return r - 1 | |
def calc_layers(ngon, memo=[()]): | |
while len(memo) < ngon+1: | |
row = [(d, *memo[len(memo)//d-1]) for d in divisors(len(memo))] | |
memo += [min(row, key=lambda r: (sum(r), len(r)))] | |
return memo[ngon] | |
class Polygon(object): | |
def __init__(self, data): | |
if isinstance(data, int): | |
self.ngon = data | |
self.rel_pieces = calc_layers(data) | |
elif isinstance(data, list) or isinstance(data, iter): | |
self.rel_pieces = data | |
self.ngon = calc_ngon(data) | |
else: | |
raise KeyError('initial data for polygon must be int or list of int.') | |
if self.ngon < 3: | |
raise KeyError('its absurd to draw a polygon with number of sides less than 3.') | |
self.angle = 360/self.ngon | |
self.abs_pieces = [self.rel_pieces[0]] | |
self.tot_pieces = [self.rel_pieces[0]] | |
self._init_pieces_info() | |
def _init_pieces_info(self): | |
for i in range(1, len(self.rel_pieces)): | |
self.abs_pieces += [self.abs_pieces[i-1] * self.rel_pieces[i]] | |
self.tot_pieces += [self.tot_pieces[i-1] + self.abs_pieces[i]] | |
def base_group(self, radius, padding): | |
center = radius + padding | |
x = center - (radius * tan(radians(self.angle/2))) | |
y = padding | |
spec = [] | |
for _ in range(self.rel_pieces[0]+1): | |
spec += f'{round(x, PRECISION)},{round(y, PRECISION)}', | |
x, y = rotate(x, y, radians(self.angle), center) | |
return f'<g id="a0"><polyline points="{" ".join(spec)}" stroke="#000" fill="none" /></g>\n' | |
def make_group(self, k, radius, padding, precision): | |
if k == 0: | |
return self.base_group(radius, padding) | |
center = radius + padding | |
theta = self.angle * self.abs_pieces[k-1] | |
s = f'<g id="a{k}">\n' | |
for i in range(1, 1+self.rel_pieces[k]): | |
rot = round(theta*i, precision) | |
s += f'<use xlink:href="#a{k-1}" transform="rotate({rot} {center} {center})" />\n' | |
s += '</g>\n' | |
return s | |
def svg(self, radius, padding, precision): | |
size = 2 * (radius + padding) | |
header = 'xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"' | |
s = f'<!-- {self.ngon}-gon with layers {self.rel_pieces}. -->\n' | |
s += f'<svg {header} height="{size}" width="{size}">\n' | |
for i, _ in enumerate(self.rel_pieces): | |
s += self.make_group(i, radius, padding, precision) | |
s += '</svg>' | |
return s | |
if __name__ == '__main__': | |
if len(sys.argv) == 2: | |
print(Polygon(int(sys.argv[1])).svg(RADIUS, PADDING, PRECISION)) | |
elif len(sys.argv) > 2: | |
print(Polygon(list(map(int, sys.argv[1:]))).svg(RADIUS, PADDING, PRECISION)) | |
elif 'LAYERS' in globals(): | |
print(Polygon(LAYERS).svg(RADIUS, PADDING, PRECISION)) | |
elif 'NGON' in globals(): | |
print(Polygon(NGON).svg(RADIUS, PADDING, PRECISION)) | |
else: | |
raise NotImplementedError |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment