"""Conway operators on a cube, drawn as SVG. Plain Python, nothing to install.

    python3 conway.py

writes conway-operators.svg (cube, d, k, t) and conway-art.svg (dktkt cube).
From Geometridae's blog, "Conway operators in Python".
"""
import re
from math import cos, sin, sqrt


def cube():
    vertices = [(x, y, z) for x in (-1, 1) for y in (-1, 1) for z in (-1, 1)]
    faces = [[0, 1, 3, 2], [4, 6, 7, 5], [0, 4, 5, 1],
             [2, 3, 7, 6], [0, 2, 6, 4], [1, 5, 7, 3]]
    return vertices, faces


def centroid(points):
    n = len(points)
    return tuple(sum(p[i] for p in points) / n for i in range(3))


def on_sphere(p):
    length = sqrt(sum(c * c for c in p))
    return tuple(c / length for c in p)


def dual(vertices, faces):
    """A vertex for every face, a face for every vertex."""
    new_vertices = [on_sphere(centroid([vertices[i] for i in f])) for f in faces]
    face_of_edge = {}                      # directed edge (a, b) -> the face it belongs to
    for fi, f in enumerate(faces):
        for a, b in zip(f, f[1:] + f[:1]):
            face_of_edge[a, b] = fi
    new_faces = []
    for v in range(len(vertices)):
        start = next(fi for fi, f in enumerate(faces) if v in f)
        ring, fi = [], start
        while True:                        # walk around v, one face at a time
            ring.append(fi)
            f = faces[fi]
            before = f[f.index(v) - 1]     # the edge (before, v) is shared with the next face
            fi = face_of_edge[v, before]
            if fi == start:
                break
        new_faces.append(ring)
    return new_vertices, new_faces


def kis(vertices, faces):
    """Raise a pyramid on every face."""
    new_vertices = list(vertices)
    new_faces = []
    for f in faces:
        apex = len(new_vertices)
        new_vertices.append(on_sphere(centroid([vertices[i] for i in f])))
        for a, b in zip(f, f[1:] + f[:1]):
            new_faces.append([a, b, apex])
    return [on_sphere(p) for p in new_vertices], new_faces


def truncate(vertices, faces):
    """Cut off every corner: dual, kis, dual."""
    return dual(*kis(*dual(vertices, faces)))


def svg(vertices, faces, size=300, turn=(0.6, 0.5)):
    """Draw the faces that point at us, lighter when they face the light."""
    a, b = turn
    def rotate(p):
        x, y, z = p
        x, z = x * cos(a) + z * sin(a), -x * sin(a) + z * cos(a)
        y, z = y * cos(b) - z * sin(b), y * sin(b) + z * cos(b)
        return x, y, z
    points = [rotate(on_sphere(p)) for p in vertices]
    shapes = []
    for f in faces:
        p, q, r = (points[i] for i in f[:3])
        u = [q[i] - p[i] for i in range(3)]
        w = [r[i] - p[i] for i in range(3)]
        normal = (u[1] * w[2] - u[2] * w[1], u[2] * w[0] - u[0] * w[2], u[0] * w[1] - u[1] * w[0])
        if normal[2] <= 0:
            continue                       # facing away
        light = normal[2] / sqrt(sum(c * c for c in normal))
        pink = f'hsl(340, 55%, {45 + 40 * light:.0f}%)'
        corners = ' '.join(f'{size / 2 * (1 + 0.9 * points[i][0]):.1f},'
                           f'{size / 2 * (1 - 0.9 * points[i][1]):.1f}' for i in f)
        shapes.append(f'<polygon points="{corners}" fill="{pink}" '
                      f'stroke="#4b3a4f" stroke-width="1" stroke-linejoin="round"/>')
    return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {size} {size}">'
            + ''.join(shapes) + '</svg>')


def side_by_side(drawings, size=300, gap=20):
    """Several square SVGs in one row."""
    width = len(drawings) * size + (len(drawings) - 1) * gap
    inside = ''.join(f'<g transform="translate({i * (size + gap)},0)">'
                     + re.sub(r'^<svg[^>]*>|</svg>$', '', d) + '</g>'
                     for i, d in enumerate(drawings))
    return f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width} {size}">{inside}</svg>'


if __name__ == '__main__':
    c = cube()
    with open('conway-operators.svg', 'w') as f:
        f.write(side_by_side([svg(*s) for s in (c, dual(*c), kis(*c), truncate(*c))]))
    with open('conway-art.svg', 'w') as f:
        f.write(svg(*dual(*kis(*truncate(*kis(*truncate(*c)))))).replace('<svg ', '<svg width="300" ', 1))
    # try your own: svg(*kis(*kis(*dual(*c)))) ...
