Formas Geométricas Redux 01

2026-07-25

"""2026-07-25
Formas Geométricas Redux 01
Exercício de grade de formas geométricas
ericof.com|https://ericof.com/en/sketches/2024-06-29
png
Sketch,py5,CreativeCoding,Vornoi
"""

from opensimplex import OpenSimplex
from scipy.spatial import Voronoi
from shapely import make_valid
from shapely.geometry import Polygon
from sketches.padroes.cacos import gera_sementes
from sketches.padroes.poligonos import vertices_poligono_regular
from sketches.utils.draw import canvas
from sketches.utils.draw.cores.paletas import gera_paleta
from sketches.utils.draw.grade import cria_grade_ex
from sketches.utils.helpers import sketches as helpers

import math
import numpy as np
import py5


sketch = helpers.info_for_sketch(__file__, __doc__)

cor_fundo = py5.color(0)

celula_x: int = 80
celula_y: int = 20
# Escala aplicada ao poligono antes do recorte. Ate ~0.9 o poligono cabe
# inteiro na sua celula (aparecem folgas); a partir de ~1.0 ele transborda e e
# recortado na fronteira de Voronoi, fazendo formas vizinhas compartilharem a
# aresta -- e ai que elas "se interseccionam". ~1.15 mantem o caráter do
# poligono e ja recorta as sobreposicoes.
forma_escala: float = 1.50
alternada: bool = True
preencher: bool = False

# Lados dos poligonos sorteados por posicao (indice (idx + idy) na grade).
LADOS: tuple[int, ...] = (8, 6, 8, 6, 6, 7, 8)

# Jitter opcional das sementes (0 = mosaico geometrico limpo; aumente para
# celulas organicas). mult desloca por noise; bagunca desloca aleatoriamente.
jitter_mult: float = 0.0
jitter_bagunca: int = 0

# Distancia do anel de sementes-sentinela alem da area visivel. Precisa superar
# o deslocamento maximo de uma semente (jitter_mult + jitter_bagunca) para que
# toda celula visivel fique fechada e o recorte nunca falhe por celula aberta.
margem_guarda: int = 300

traco: float = 3.4
cor_traco_preenchido = py5.color(255, 255, 255, 0.8)
traco_preenchido: float = 1.5

noise_generator = OpenSimplex(seed=py5.random_int(20_000))

formas: list[py5.Py5Shape] = []


def celula_como_poligono(vor: Voronoi, indice: int) -> Polygon | None:
    """Extrai a celula de Voronoi de uma semente como poligono shapely.

    A regiao de Voronoi devolve os vertices sem ordem garantida; como a celula e
    convexa, ordena-los angularmente em torno do centroide descreve o poligono
    simples. Sementes internas sempre tem celula fechada gracas ao anel-sentinela.

    :param vor: Diagrama de Voronoi ja calculado.
    :param indice: Indice da semente em ``vor.point_region``.
    :returns: A celula como :class:`~shapely.geometry.Polygon` valida, ou
        ``None`` se a regiao for aberta/degenerada.
    """
    regiao = vor.regions[vor.point_region[indice]]
    if not regiao or -1 in regiao:
        return None
    verts = [tuple(vor.vertices[v]) for v in regiao]
    cx = sum(v[0] for v in verts) / len(verts)
    cy = sum(v[1] for v in verts) / len(verts)
    verts.sort(key=lambda v: math.atan2(v[1] - cy, v[0] - cx))
    return make_valid(Polygon(verts))


def poligono_na_semente(cx: float, cy: float, lados: int) -> Polygon:
    """Constroi o poligono regular escalado, centrado na semente.

    Os vertices vem centrados em ``(tam / 2, tam / 2)``; sao transladados para a
    origem e reposicionados em ``(cx, cy)`` para casar com a celula (que vive em
    coordenadas absolutas).

    :param cx: Coordenada x do centro (a semente).
    :param cy: Coordenada y do centro (a semente).
    :param lados: Numero de lados do poligono regular.
    :returns: O poligono regular escalado como :class:`~shapely.geometry.Polygon`.
    """
    tam_x = celula_x * forma_escala
    tam_y = celula_y * forma_escala
    verts = vertices_poligono_regular(tam_x, tam_y, lados)
    verts = verts - [tam_x / 2, tam_y / 2] + [cx, cy]
    return make_valid(Polygon(verts))


def forma_do_recorte(geom) -> py5.Py5Shape | None:
    """Converte a interseccao poligono-celula numa ``Py5Shape`` fechada.

    A interseccao de dois convexos e um unico poligono convexo; ainda assim
    tratamos ``MultiPolygon`` / ``GeometryCollection`` por seguranca, tomando o
    maior anel. Retorna ``None`` quando nada sobra (poligono fora da celula).

    :param geom: Geometria shapely resultante do ``intersection``.
    :returns: A forma fechada em coordenadas absolutas, ou ``None`` se vazia.
    """
    if geom.is_empty:
        return None
    if geom.geom_type == "Polygon":
        maior = geom
    else:
        poligonos = [g for g in getattr(geom, "geoms", []) if g.geom_type == "Polygon"]
        if not poligonos:
            return None
        maior = max(poligonos, key=lambda g: g.area)
    forma = py5.create_shape()
    with forma.begin_closed_shape():
        for x, y in maior.exterior.coords[:-1]:
            forma.vertex(x, y)
    return forma


def inicializa():
    global formas
    grade_ex = cria_grade_ex(
        *helpers.DIMENSOES.external, 0, 0, celula_x, celula_y, alternada=alternada
    )
    grade = [(x, y) for _, x, _, y in grade_ex]
    lados_por_semente = [LADOS[(idx + idy) % len(LADOS)] for idx, _, idy, _ in grade_ex]

    pontos, n_internas = gera_sementes(
        *helpers.DIMENSOES.external,
        celula_x,
        celula_y,
        grade,
        jitter_mult,
        jitter_mult,
        jitter_bagunca,
        margem_guarda,
        noise_generator,
    )
    vor = Voronoi(np.array(pontos))

    formas = []
    for i in range(n_internas):
        celula = celula_como_poligono(vor, i)
        if celula is None:
            continue
        cx, cy = pontos[i]
        poligono = poligono_na_semente(cx, cy, lados_por_semente[i])
        forma = forma_do_recorte(poligono.intersection(celula))
        if forma is not None:
            formas.append(forma)


def setup():
    py5.size(*helpers.DIMENSOES.external, py5.P3D)
    inicializa()


def draw():
    py5.background(cor_fundo)
    paleta = gera_paleta("bright-colors", True)
    with py5.push():
        py5.translate(0, 0, -10)
        for forma in formas:
            if preencher:
                forma.set_fill(paleta[0])
                forma.set_stroke_weight(traco_preenchido)
                forma.set_stroke(cor_traco_preenchido)
            else:
                forma.set_fill(False)
                forma.set_stroke(paleta[0])
                forma.set_stroke_weight(traco)
            py5.shape(forma)
            paleta.rotate(1)

    msg = (
        f"escala: {forma_escala:.2f} | "
        f"celulas: {len(formas)} | jitter: {jitter_bagunca}"
    )
    # Credits and go
    canvas.sketch_frame(
        sketch,
        cor_fundo,
        "large_transparent_white",
        "transparent_white",
        version=2,
        msg=msg,
    )


def key_pressed():
    global forma_escala, preencher, jitter_bagunca
    key = py5.key
    match key:
        case " ":
            save_and_close()
        case "r":
            inicializa()
        case "f":
            preencher = not preencher
        case "+" | "-":
            forma_escala = max(0.5, forma_escala + (0.05 if key == "+" else -0.05))
            inicializa()
        case "j" | "k":
            jitter_bagunca = max(0, jitter_bagunca + (5 if key == "j" else -5))
            inicializa()


def save_and_close():
    py5.no_loop()
    canvas.save_sketch_image(sketch)
    py5.exit_sketch()


if __name__ == "__main__":
    py5.run_sketch()