"""Kit de blocos do hero: um bloco chanfrado e quatro estados com o mesmo N. Rodar da raiz do repositório: blender -b --python 3d/blocos.py Gera public/3d/bloco.glb, public/3d/blocos.json, os dois matcaps, o pôster do hero e os dos estados (Cycles) e, em 3d/out/, o .blend e as prévias de cada estado. Coordenadas do JSON: Y para cima, peça de frente para +Z, centrada na origem. """ import ctypes import gzip import math import os import sys import bmesh import bpy import numpy as np from bpy_extras.object_utils import world_to_camera_view from mathutils import Vector RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PUBLICO = os.path.join(RAIZ, "public", "3d") SAIDA = os.path.join(RAIZ, "3d", "out") GLB = os.path.join(PUBLICO, "bloco.glb") JSON = os.path.join(PUBLICO, "blocos.json") ESTADOS = ["marca", "tela", "integracoes", "dados"] PASSO = 7 # números por bloco: x, y, z, sx, sy, sz, tom CAIXA = (16, 11, 4) # ---------------------------------------------------------------- marca # public/brand/mark-dark.svg (viewBox 210 220 830 800) POLIGONO = [ (245, 757), (321, 757), (321, 681), (399, 681), (399, 606), (476, 606), (476, 757), (399, 757), (399, 878), (523, 878), (523, 757), (654, 757), (654, 913), (594, 913), (594, 982), (245, 982), ] # a forma branca em retângulos exatos (u0, v0, u1, v1), sem sobreposição BRANCOS = [ (399, 606, 476, 757), (321, 681, 399, 757), (245, 757, 399, 982), (523, 757, 594, 878), (594, 757, 654, 913), (399, 878, 594, 982), ] LIMAS = [ (511, 512, 667, 688), (667, 358, 821, 512), (844, 255, 947, 358), (906, 432, 1007, 533), ] LADO_SVG = 38 ESCALA = 10 / 727 # altura da marca (255..982) vira 10 CU, CV = (245 + 1007) / 2, (255 + 982) / 2 # centro da marca no SVG def svg_para_mundo(u, v): return (u - CU) * ESCALA, -(v - CV) * ESCALA def estado_marca(): sz = LADO_SVG * ESCALA blocos = [] for tom, retangulos in ((0, BRANCOS), (1, LIMAS)): for u0, v0, u1, v1 in retangulos: nx = max(1, round((u1 - u0) / LADO_SVG)) ny = max(1, round((v1 - v0) / LADO_SVG)) bu, bv = (u1 - u0) / nx, (v1 - v0) / ny for i in range(nx): for j in range(ny): x, y = svg_para_mundo(u0 + (i + 0.5) * bu, v0 + (j + 0.5) * bv) for z in (-sz / 2, sz / 2): blocos.append((x, y, z, bu * ESCALA, bv * ESCALA, sz, tom)) return blocos # ------------------------------------------- tela, integrações, dados # Desenhados numa grade inteira (coluna i, linha j com j = 0 no topo, camada k # para +Z). O passo em mundo sai do tamanho da grade; a densidade de cada # desenho foi escolhida para ficar entre 85% e 100% de N. class Grade: def __init__(self): self.celulas = {} def bloco(self, i, j, k=0, tom=0): # lima prevalece quando dois elementos caem na mesma célula chave = (round(i), round(j), k) self.celulas[chave] = max(tom, self.celulas.get(chave, 0)) def cheio(self, i0, j0, i1, j1, k=0, tom=0): for i in range(i0, i1 + 1): for j in range(j0, j1 + 1): self.bloco(i, j, k, tom) def contorno(self, i0, j0, i1, j1, k=0, tom=0): for i in range(i0, i1 + 1): self.bloco(i, j0, k, tom) self.bloco(i, j1, k, tom) for j in range(j0 + 1, j1): self.bloco(i0, j, k, tom) self.bloco(i1, j, k, tom) def linha(self, a, b, k=0, tom=0): n = math.ceil(max(abs(b[0] - a[0]), abs(b[1] - a[1]))) * 2 + 1 for t in range(n + 1): f = t / n self.bloco( math.floor(a[0] + (b[0] - a[0]) * f + 0.5), math.floor(a[1] + (b[1] - a[1]) * f + 0.5), k, tom, ) def disco(self, ci, cj, r, k=0, tom=0): n = math.floor(r) for di in range(-n, n + 1): for dj in range(-n, n + 1): if di * di + dj * dj <= r * r: self.bloco(ci + di, cj + dj, k, tom) def blocos(self): cel = self.celulas ii = [c[0] for c in cel] jj = [c[1] for c in cel] kk = [c[2] for c in cel] largura = max(ii) - min(ii) + 1 altura = max(jj) - min(jj) + 1 camadas = max(kk) - min(kk) + 1 # maior passo que cabe na caixa, com folga p = min(14.5 / largura, 9.8 / altura, 3.8 / camadas) mi, mj, mk = (max(ii) + min(ii)) / 2, (max(jj) + min(jj)) / 2, (max(kk) + min(kk)) / 2 return [ ((i - mi) * p, -(j - mj) * p, (k - mk) * p, p, p, p, tom) for (i, j, k), tom in sorted(cel.items()) ] def estado_tela(): # referência: drawScreen em app/components/cinematic/HeroFieldScene.tsx g = Grade() C, R = 26, 18 g.contorno(0, 0, C - 1, R - 1) # moldura for i in (2, 4, 6): g.bloco(i, 1) # três pontos g.cheio(1, 2, C - 2, 2) # barra do navegador g.cheio(2, 4, 9, 5) # título g.cheio(2, 7, 12, 7) # texto g.cheio(2, 9, 9, 9) for k in (0, 1): g.cheio(2, 11, 7, 11, k, tom=1) # botão, saltado para a frente g.contorno(15, 4, C - 3, 11) # imagem g.linha((16, 10), (19, 7)) g.linha((19, 7), (21, 9)) g.bloco(21, 6) for i0 in (2, 10, 18): g.contorno(i0, 13, i0 + 5, 15) # cartões return g.blocos() def estado_integracoes(): # referência: drawIntegrations em HeroFieldScene.tsx g = Grade() A, B = 11, 7.5 # semieixos da elipse dos nós r_nucleo, r_no = 2.6, 1.6 for k in (0, 1): g.disco(0, 0, r_nucleo, k, tom=1) nos = [] for n in range(6): a = n / 6 * math.tau + math.pi / 6 nos.append((round(math.cos(a) * A), round(math.sin(a) * B))) for n, (x, y) in enumerate(nos): for k in (0, 1): g.disco(x, y, r_no, k) g.bloco(x, y, 1, tom=1) d = math.hypot(x, y) ux, uy = x / d, y / d g.linha((ux * (r_nucleo + 0.5), uy * (r_nucleo + 0.5)), (x - ux * (r_no + 0.5), y - uy * (r_no + 0.5))) nx, ny = nos[(n + 1) % 6] dx, dy = nx - x, ny - y d = math.hypot(dx, dy) g.linha((x + dx / d * (r_no + 0.5), y + dy / d * (r_no + 0.5)), (nx - dx / d * (r_no + 0.5), ny - dy / d * (r_no + 0.5))) return g.blocos() def estado_dados(): # referência: drawData em HeroFieldScene.tsx; a coluna da direita vira barras em +Z g = Grade() C, R = 24, 15 g.contorno(0, 0, C - 1, R - 1) g.cheio(1, 1, C - 2, 2, tom=1) # cabeçalho colunas = (7, 13, 18) for c in colunas: g.cheio(c, 3, c, R - 2) valores = ((5, 3, 2), (3, 4, 3), (4, 2, 1), (2, 4, 3), (5, 3, 2)) alturas = (2, 4, 1, 5, 3) for linha, (v, h) in enumerate(zip(valores, alturas)): j = 4 + linha * 2 for inicio, comprimento in zip((2, 9, 15), v): g.cheio(inicio, j, inicio + comprimento - 1, j) for k in range(h + 1): g.cheio(20, j, 21, j, k, tom=int(k == h)) # topo da barra em lima return g.blocos() # ------------------------------------------------ montagem dos estados def chave(b): return (round(b[0], 3), round(b[1], 3), round(b[2], 3)) def completar(blocos, n): """Ordena por (x, y, z) e põe os que faltam para n ocultos sobre visíveis espalhados.""" visiveis = sorted(blocos, key=chave) v, h = len(visiveis), n - len(visiveis) if h < 0: falhar(f"estado com {v} blocos, mais que N={n}") ocultos = [] for m in range(h): x, y, z, _, _, _, tom = visiveis[(m * 2 + 1) * v // (h * 2)] ocultos.append((x, y, z, 0, 0, 0, tom)) # oculto fica logo depois do visível em que está, mesma chave espacial return sorted(visiveis + ocultos, key=lambda b: (chave(b), b[3] == 0)) def fmt(v): s = f"{v:.3f}" return "0.000" if s == "-0.000" else s # ------------------------------------------------------------- Blender def srgb(h): def lin(c): c /= 255 return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 return (*(lin(int(h[i : i + 2], 16)) for i in (0, 2, 4)), 1.0) def criar_bloco(): malha = bpy.data.meshes.new("bloco") bm = bmesh.new() bmesh.ops.create_cube(bm, size=1) bm.to_mesh(malha) bm.free() obj = bpy.data.objects.new("bloco", malha) bpy.context.scene.collection.objects.link(obj) bpy.context.view_layer.objects.active = obj obj.select_set(True) chanfro = obj.modifiers.new("chanfro", "BEVEL") chanfro.width = 0.08 chanfro.segments = 2 malha.shade_smooth() pesos = obj.modifiers.new("normais", "WEIGHTED_NORMAL") pesos.keep_sharp = True for m in ("chanfro", "normais"): bpy.ops.object.modifier_apply(modifier=m) return obj def exportar_glb(obj): bpy.ops.object.select_all(action="DESELECT") obj.select_set(True) bpy.ops.export_scene.gltf( filepath=GLB, export_format="GLB", use_selection=True, export_yup=True, export_apply=True, export_normals=True, export_texcoords=False, export_tangents=False, export_materials="NONE", export_vertex_color="NONE", export_attributes=False, export_extras=False, export_animations=False, export_draco_mesh_compression_enable=False, ) def montar_cena(estados, malha): cena = bpy.context.scene materiais = criar_materiais() malha.materials.append(materiais[0]) colecoes = {} for nome, blocos in zip(ESTADOS, estados): col = bpy.data.collections.new(nome) cena.collection.children.link(col) colecoes[nome] = col for n, (x, y, z, sx, sy, sz, tom) in enumerate(blocos): obj = bpy.data.objects.new(f"{nome}.{n:03d}", malha) col.objects.link(obj) # arquivo Y-up/+Z frente -> Blender Z-up/-Y frente obj.location = (x, -z, y) obj.scale = (sx, sz, sy) obj.material_slots[0].link = "OBJECT" obj.material_slots[0].material = materiais[tom] obj.hide_render = obj.hide_viewport = sx == 0 return colecoes, materiais def renderizar(colecoes): cena = bpy.context.scene cena.render.engine = "BLENDER_WORKBENCH" cena.render.resolution_x, cena.render.resolution_y = 800, 600 cena.render.resolution_percentage = 100 cena.render.image_settings.file_format = "PNG" cena.view_settings.view_transform = "Standard" cena.display.shading.light = "STUDIO" cena.display.shading.color_type = "MATERIAL" cena.display.shading.show_cavity = True cena.world = bpy.data.worlds.new("fundo") cena.world.color = (0.008, 0.008, 0.01) cam = bpy.data.objects.new("camera", bpy.data.cameras.new("camera")) cena.collection.objects.link(cam) cam.location = (-6.0, -25.0, 3.0) # pela esquerda: barras de dados saem para fora da tabela cam.rotation_euler = (-cam.location.normalized()).to_track_quat("-Z", "Y").to_euler() cena.camera = cam for nome in ESTADOS: for outro, col in colecoes.items(): col.hide_render = col.hide_viewport = outro != nome cena.render.filepath = os.path.join(SAIDA, f"previa-{nome}.png") bpy.ops.render.render(write_still=True) for outro, col in colecoes.items(): col.hide_render = col.hide_viewport = outro != "marca" # ------------------------------------------------------- render final # Porcelana fosca com um verniz fino e esmalte lima com emissão leve, numa luz de estúdio de três pontos. # Os matcaps (uma esfera de cada material, vista de frente) levam essa mesma luz para o tempo real. def criar_materiais(): def material(nome, cor, **entradas): mat = bpy.data.materials.new(nome) mat.diffuse_color = srgb(cor) # cor da prévia (Workbench) bsdf = mat.node_tree.nodes["Principled BSDF"] bsdf.inputs["Base Color"].default_value = srgb(cor) for entrada, valor in entradas.items(): bsdf.inputs[entrada].default_value = valor return mat return { 0: material("porcelana", "f4f3ef", **{"Roughness": 0.5, "Coat Weight": 0.12, "Coat Roughness": 0.3}), 1: material( "lima", "d5fa00", **{ "Roughness": 0.28, "Coat Weight": 0.3, "Coat Roughness": 0.12, "Emission Color": srgb("d5fa00"), "Emission Strength": 0.06, }, ), } def olhar_para_a_origem(obj): obj.rotation_euler = (-Vector(obj.location).normalized()).to_track_quat("-Z", "Y").to_euler() def enquadrar(camera, objetos, direcao, ocupa): """Põe a câmera na direção dada, olhando a origem, na distância em que os objetos ocupam essa fração do quadro.""" cena = bpy.context.scene cantos = [obj.matrix_world @ Vector(c) for obj in objetos for c in obj.bound_box] distancia = 40 for _ in range(3): # com lente longa o tamanho na tela é quase 1 / distância: três passos convergem camera.location = Vector(direcao).normalized() * distancia olhar_para_a_origem(camera) bpy.context.view_layer.update() vistos = [world_to_camera_view(cena, camera, p) for p in cantos] distancia *= 2 * max(max(abs(p.x - 0.5), abs(p.y - 0.5)) for p in vistos) / ocupa def luz(nome, posicao, lado, potencia, cor=(1, 1, 1)): dados = bpy.data.lights.new(nome, "AREA") dados.shape = "SQUARE" dados.size = lado dados.energy = potencia dados.color = cor obj = bpy.data.objects.new(nome, dados) bpy.context.scene.collection.objects.link(obj) obj.location = posicao olhar_para_a_origem(obj) def ligar_cycles(cena): """Cycles na GPU com OptiX quando a máquina tem; senão segue na CPU.""" cena.render.engine = "CYCLES" cena.cycles.use_denoising = True prefs = bpy.context.preferences.addons["cycles"].preferences try: prefs.compute_device_type = "OPTIX" except TypeError: return prefs.get_devices_for_type("OPTIX") for aparelho in prefs.devices: aparelho.use = aparelho.type == "OPTIX" if any(aparelho.use for aparelho in prefs.devices): cena.cycles.device = "GPU" cena.cycles.denoiser = "OPTIX" def sair(cena, caminho, lado, formato, modo, amostras, transparente): cena.render.resolution_x, cena.render.resolution_y = lado cena.render.film_transparent = transparente cena.render.image_settings.file_format = formato cena.render.image_settings.color_mode = modo cena.render.image_settings.quality = 84 cena.cycles.samples = amostras cena.render.filepath = caminho bpy.ops.render.render(write_still=True) def luz_de_estudio(cena): fundo = cena.world.node_tree.nodes["Background"] fundo.inputs["Color"].default_value = (0.02, 0.022, 0.02, 1) fundo.inputs["Strength"].default_value = 1.0 # três tons por bloco: topo claro (principal, quase a pino), frente média (preenchimento baixo e frontal) e a lateral # que a câmera vê na sombra; o contraluz recorta a silhueta por trás luz("principal", (6, -10, 20), 14, 7000) luz("preenchimento", (-10, -18, 3), 18, 2000, (0.92, 0.96, 1.0)) luz("contraluz", (-8, 12, 10), 10, 2000, (0.85, 1.0, 0.75)) def render_final(colecoes, materiais): cena = bpy.context.scene ligar_cycles(cena) luz_de_estudio(cena) def so(nome): for outro, col in colecoes.items(): col.hide_render = col.hide_viewport = outro != nome # prévias finais dos quatro estados, na câmera das prévias rápidas for nome in ESTADOS: so(nome) sair(cena, os.path.join(SAIDA, f"final-{nome}.png"), (800, 600), "PNG", "RGB", 64, False) camera = bpy.data.objects.new("camera-poster", bpy.data.cameras.new("camera-poster")) cena.collection.objects.link(camera) camera.data.lens = 85 cena.camera = camera # pôsteres dos estados: quase de frente como as prévias, mas de lente longa e com a peça em uns 85% do quadro cena.render.resolution_x, cena.render.resolution_y = 1200, 900 # o enquadramento usa a proporção do quadro for nome in ESTADOS[1:]: so(nome) enquadrar(camera, [obj for obj in colecoes[nome].objects if not obj.hide_render], (-6, -25, 3), 0.85) sair(cena, os.path.join(PUBLICO, f"estado-{nome}.webp"), (1200, 900), "WEBP", "RGBA", 256, True) # pôster: a marca em três quartos, fundo transparente camera.location = Vector((-0.42, -0.86, 0.2)).normalized() * 36 olhar_para_a_origem(camera) so("marca") sair(cena, os.path.join(PUBLICO, "hero-poster.webp"), (1200, 1200), "WEBP", "RGBA", 256, True) # matcaps: uma esfera de cada material, de frente, em vista ortográfica so(None) malha = bpy.data.meshes.new("esfera") bm = bmesh.new() bmesh.ops.create_uvsphere(bm, u_segments=96, v_segments=48, radius=1) bm.to_mesh(malha) bm.free() malha.shade_smooth() esfera = bpy.data.objects.new("esfera", malha) cena.collection.objects.link(esfera) frente = bpy.data.objects.new("camera-matcap", bpy.data.cameras.new("camera-matcap")) cena.collection.objects.link(frente) frente.data.type = "ORTHO" frente.data.ortho_scale = 2.0 frente.location = (0, -12, 0) olhar_para_a_origem(frente) cena.camera = frente malha.materials.append(None) # a esfera de frente pega mais luz de preenchimento que as faces do pôster: a porcelana sai 0,3 ponto de exposição # abaixo, para a cena em tempo real ter o mesmo branco do pôster (a lima já bate) for tom, nome, exposicao in ((0, "porcelana", -0.3), (1, "lima", 0.0)): malha.materials[0] = materiais[tom] cena.view_settings.exposure = exposicao sair(cena, os.path.join(PUBLICO, f"matcap-{nome}.webp"), (256, 256), "WEBP", "RGB", 128, False) cena.view_settings.exposure = 0.0 bpy.data.objects.remove(esfera) bpy.data.objects.remove(frente) cena.camera = camera so("marca") # ------------------------------------------------------- autoconferência def falhar(msg): print(f"FALHOU: {msg}", flush=True) sys.exit(1) def conferir(tris): with open(JSON, "rb") as f: bruto = f.read() import json doc = json.loads(bruto) n = doc["n"] dados = np.array(doc["dados"], dtype=float) if dados.size != len(ESTADOS) * n * PASSO: falhar(f"dados com {dados.size} números, esperado {len(ESTADOS) * n * PASSO} (mesmo N em todos os estados)") dados = dados.reshape(len(ESTADOS), n, PASSO) if n > 320: falhar(f"N={n} > 320") if n * tris > 60000: falhar(f"N x triângulos = {n * tris} > 60000") if os.path.getsize(GLB) > 40 * 1024: falhar(f"bloco.glb com {os.path.getsize(GLB)} bytes > 40 KB") estados = [(f"estado-{nome}.webp", 90) for nome in ESTADOS[1:]] for nome, limite in (("hero-poster.webp", 90), *estados, ("matcap-porcelana.webp", 30), ("matcap-lima.webp", 30)): tamanho = os.path.getsize(os.path.join(PUBLICO, nome)) if tamanho > limite * 1024: falhar(f"{nome} com {tamanho} bytes > {limite} KB") if len(gzip.compress(bruto)) > 40 * 1024: falhar("blocos.json em gzip > 40 KB") meia = np.array(CAIXA) / 2 + 1e-3 for nome, e in zip(ESTADOS, dados): vis = e[e[:, 3] > 0] ocultos = n - len(vis) if ocultos > 0.15 * n: falhar(f"{nome}: {ocultos} ocultos > 15% de {n}") if np.any(np.abs(vis[:, :3]) + vis[:, 3:6] / 2 > meia): falhar(f"{nome}: sai da caixa {CAIXA}") if nome != "marca": if np.any(np.abs(vis[:, 3] - vis[:, 4]) > 1e-3) or np.any(np.abs(vis[:, 3] - vis[:, 5]) > 1e-3): falhar(f"{nome}: blocos não cúbicos") if vis[:, 6].sum() > 0.25 * len(vis): falhar(f"{nome}: lima em {int(vis[:, 6].sum())} de {len(vis)} visíveis > 25%") # marca: em cada camada, os blocos lima ladrilham os quatro retângulos com tolerância # de 0.5 unidade do SVG: cada bloco dentro de um retângulo, sem sobreposição e com a # área de cada retângulo preenchida (folga de 0.5 em todo o perímetro). tol = 0.5 marca = dados[0] area_poligono = abs(sum( a[0] * b[1] - b[0] * a[1] for a, b in zip(POLIGONO, POLIGONO[1:] + POLIGONO[:1]) )) / 2 for z in np.unique(marca[:, 2]): camada = marca[marca[:, 2] == z] u = camada[:, 0] / ESCALA + CU v = -camada[:, 1] / ESCALA + CV lu = camada[:, 3] / ESCALA lv = camada[:, 4] / ESCALA caixas = [(u[b] - lu[b] / 2, v[b] - lv[b] / 2, u[b] + lu[b] / 2, v[b] + lv[b] / 2) for b in np.flatnonzero(camada[:, 6] == 1)] areas = [0.0] * len(LIMAS) for c in caixas: dentro = [r for r, (u0, v0, u1, v1) in enumerate(LIMAS) if c[0] >= u0 - tol and c[1] >= v0 - tol and c[2] <= u1 + tol and c[3] <= v1 + tol] if not dentro: falhar(f"marca z={z}: bloco lima {c} fora dos retângulos lima") areas[dentro[0]] += (c[2] - c[0]) * (c[3] - c[1]) for (u0, v0, u1, v1), area in zip(LIMAS, areas): if abs(area - (u1 - u0) * (v1 - v0)) > tol * 2 * (u1 - u0 + v1 - v0): falhar(f"marca z={z}: retângulo lima {(u0, v0, u1, v1)} coberto com área {area:.0f}") for a in range(len(caixas)): for b in range(a): p, q = caixas[a], caixas[b] if min(p[2], q[2]) - max(p[0], q[0]) > tol and min(p[3], q[3]) - max(p[1], q[1]) > tol: falhar(f"marca z={z}: blocos lima sobrepostos {p} {q}") brancos = camada[:, 6] == 0 area = float((lu[brancos] * lv[brancos]).sum()) if abs(area - area_poligono) > 0.01 * area_poligono: falhar(f"marca z={z}: área branca {area:.0f} != polígono {area_poligono:.0f}") def main(): bpy.ops.wm.read_factory_settings(use_empty=True) os.makedirs(PUBLICO, exist_ok=True) os.makedirs(SAIDA, exist_ok=True) marca = estado_marca() n = len(marca) estados = [completar(b, n) for b in (marca, estado_tela(), estado_integracoes(), estado_dados())] numeros = ",".join(fmt(v) for e in estados for b in e for v in b) lista = ",".join(f'"{e}"' for e in ESTADOS) with open(JSON, "w") as f: f.write(f'{{"n":{n},"passo":{PASSO},"estados":[{lista}],"dados":[{numeros}]}}') bloco = criar_bloco() bloco.data.calc_loop_triangles() tris = len(bloco.data.loop_triangles) exportar_glb(bloco) bpy.context.scene.collection.objects.unlink(bloco) # a malha fica só nas instâncias colecoes, materiais = montar_cena(estados, bloco.data) bpy.data.objects.remove(bloco) renderizar(colecoes) render_final(colecoes, materiais) bpy.context.preferences.filepaths.save_version = 0 # sem blocos.blend1 bpy.ops.wm.save_as_mainfile(filepath=os.path.join(SAIDA, "blocos.blend")) conferir(tris) for nome, e in zip(ESTADOS, estados): vis = [b for b in e if b[3] > 0] print(f"{nome}: {len(vis)} visíveis, {sum(b[6] for b in vis)} lima", flush=True) ctypes.CDLL(None).fflush(None) # despeja o buffer do C (banner do Blender) antes do OK print(f"OK blocos={n} tris={n * tris}", flush=True) sys.exit(0) # sai já, para o OK ser a última linha if __name__ == "__main__": main()