andrefbfreitas/casting-sim
0
1import os2import streamlit as st3import streamlit.components.v1 as components4import pyvista as pv5import numpy as np6import tempfile7import base648import time9import hashlib10import traceback11import gc12 13try:14 from fpdf import FPDF15 HAS_FPDF = True16except ImportError:17 HAS_FPDF = False18 19# --- SETUP DE INFRAESTRUTURA ---20os.environ['PYVISTA_OFF_SCREEN'] = 'true'21os.environ['VTK_GRAPHICS_BACKEND'] = 'OSMesa'22 23from OCC.Core.STEPControl import STEPControl_Reader24from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh25from OCC.Core.BRepGProp import brepgprop26from OCC.Core.GProp import GProp_GProps27from OCC.Core.StlAPI import StlAPI_Writer28 29st.set_page_config(page_title="CastingSim V49", layout="wide")30 31st.markdown("""32 <style>33 .stApp { animation: fadeIn 0.4s ease-in-out; }34 @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }35 </style>36""", unsafe_allow_html=True)37 38# ==========================================39# INICIALIZAÇÃO DE ESTADOS40# ==========================================41if 'step' not in st.session_state: st.session_state.step = 142if 'cache_hash' not in st.session_state: st.session_state.cache_hash = ""43if 'path_stl' not in st.session_state: st.session_state.path_stl = None44if 'path_proxy_stl' not in st.session_state: st.session_state.path_proxy_stl = None 45if 'path_resultados_proxy' not in st.session_state: st.session_state.path_resultados_proxy = None 46if 'b64_proxy' not in st.session_state: st.session_state.b64_proxy = "" 47if 'vol_mm3' not in st.session_state: st.session_state.vol_mm3 = 0.048if 'coord_iniciada' not in st.session_state: st.session_state.coord_iniciada = False49 50if 'lin_def' not in st.session_state: st.session_state.lin_def = 0.151if 'ang_def' not in st.session_state: st.session_state.ang_def = 0.152if 'sub_div' not in st.session_state: st.session_state.sub_div = 1 53 54if 'liga' not in st.session_state: st.session_state.liga = "SAE 305"55if 't_vaz' not in st.session_state: st.session_state.t_vaz = 72056if 't_molde' not in st.session_state: st.session_state.t_molde = 300 57if 'mat_molde' not in st.session_state: st.session_state.mat_molde = "Aço 1045"58if 'tinta_ceramica' not in st.session_state: st.session_state.tinta_ceramica = True59if 'espessura_tinta' not in st.session_state: st.session_state.espessura_tinta = 5.0 60if 'vel_vaz' not in st.session_state: st.session_state.vel_vaz = 1.0 61if 'diam_vaz' not in st.session_state: st.session_state.diam_vaz = 20.0 62 63if 'off_x' not in st.session_state: st.session_state.off_x = 0.064if 'off_y' not in st.session_state: st.session_state.off_y = 0.065if 'off_z' not in st.session_state: st.session_state.off_z = 0.066if 'p_riser_real' not in st.session_state: st.session_state.p_riser_real = [0.0, 0.0, 0.0]67if 'rot_x' not in st.session_state: st.session_state.rot_x = 068if 'rot_y' not in st.session_state: st.session_state.rot_y = 069if 'rot_z' not in st.session_state: st.session_state.rot_z = 070if 'eixo_vaz' not in st.session_state: st.session_state.eixo_vaz = "Z" 71 72if 'b64_termico' not in st.session_state: st.session_state.b64_termico = ""73if 'b64_rechupe_x10' not in st.session_state: st.session_state.b64_rechupe_x10 = ""74if 'b64_solidificacao' not in st.session_state: st.session_state.b64_solidificacao = "" 75if 'frames_filling' not in st.session_state: st.session_state.frames_filling = [] 76if 'frames_cooling' not in st.session_state: st.session_state.frames_cooling = [] 77if 't_sol_minutos' not in st.session_state: st.session_state.t_sol_minutos = 078if 't_sol_segundos' not in st.session_state: st.session_state.t_sol_segundos = 079if 'relatorio_ia' not in st.session_state: st.session_state.relatorio_ia = ""80if 'relatorio_pdf' not in st.session_state: st.session_state.relatorio_pdf = None81 82# --- FUNÇÕES AUXILIARES ---83def atualizar_eixo():84 st.session_state.eixo_vaz = st.session_state.eixo_sel_key85 86def compilar_pdf():87 if not HAS_FPDF: return None88 pdf = FPDF()89 pdf.add_page()90 pdf.set_font("Arial", 'B', 16)91 pdf.cell(0, 10, "LAUDO DE ENGENHARIA - CASTINGSIM V49", 0, 1, 'C')92 pdf.ln(5)93 pdf.set_font("Arial", 'B', 12)94 pdf.cell(0, 10, "1. Parametros de Processo", 0, 1)95 pdf.set_font("Arial", '', 11)96 pdf.cell(0, 6, f"Liga: {st.session_state.liga} | Vel. Vazamento: {st.session_state.vel_vaz} m/s | Derramador Ø: {st.session_state.diam_vaz} mm", 0, 1)97 pdf.cell(0, 6, f"Eixo de Gravidade Visual (Solver): {st.session_state.eixo_vaz}", 0, 1)98 pdf.cell(0, 6, f"Molde: Coquilha de {st.session_state.mat_molde}", 0, 1)99 if st.session_state.tinta_ceramica:100 pdf.cell(0, 6, f"Revestimento: Tinta Ceramica (Espessura: {st.session_state.espessura_tinta} mm)", 0, 1)101 pdf.cell(0, 6, f"Temps: Vazamento {st.session_state.t_vaz} C | Coquilha {st.session_state.t_molde} C", 0, 1)102 pdf.ln(5)103 pdf.set_font("Arial", 'B', 12)104 pdf.cell(0, 10, "2. Resultados Metrologicos", 0, 1)105 pdf.set_font("Arial", '', 11)106 pdf.cell(0, 6, f"Tempo de Solidificacao: {st.session_state.t_sol_minutos} min e {st.session_state.t_sol_segundos} s", 0, 1)107 pdf.ln(5)108 pdf.set_font("Arial", 'B', 12)109 pdf.cell(0, 10, "3. Diagnostico IA (Mestre Fundidor)", 0, 1)110 pdf.set_font("Arial", '', 11)111 texto_limpo = st.session_state.relatorio_ia.replace("*", "").replace("🌪️", ">").replace("🌊", ">").replace("🚨", ">").replace("⚡", ">").replace("✅", ">").replace("⏳", ">").replace("🐢", ">")112 pdf.multi_cell(0, 6, texto_limpo)113 return pdf.output(dest='S').encode('latin-1')114 115def gerar_hash(file_bytes, lin_def, ang_def):116 m = hashlib.md5()117 m.update(file_bytes)118 m.update(str(lin_def).encode('utf-8'))119 m.update(str(ang_def).encode('utf-8'))120 return m.hexdigest()121 122def processar_malha_com_telemetria(file_bytes, lin_def, ang_def):123 prog_bar = st.progress(0, text="⚙️ 0% - Iniciando leitura do arquivo STEP...")124 temp_dir = tempfile.mkdtemp()125 path_in = os.path.join(temp_dir, "peca.step")126 with open(path_in, "wb") as f: f.write(file_bytes)127 reader = STEPControl_Reader()128 reader.ReadFile(path_in)129 reader.TransferRoots()130 shape = reader.OneShape()131 prog_bar.progress(20, text="📐 20% - Extraindo geometria B-Rep...")132 props = GProp_GProps()133 brepgprop.VolumeProperties(shape, props)134 vol_mm3 = props.Mass()135 prog_bar.progress(60, text=f"🕸️ 60% - Tesselação CAD Base...")136 mesh_gen = BRepMesh_IncrementalMesh(shape, lin_def, False, ang_def, True)137 mesh_gen.Perform()138 path_stl = os.path.join(temp_dir, "malha_alta.stl")139 StlAPI_Writer().Write(shape, path_stl)140 mesh_alta = pv.read(path_stl)141 path_proxy = os.path.join(temp_dir, "malha_proxy.stl")142 try:143 if mesh_alta.n_faces > 3000:144 fator = 1.0 - (3000.0 / mesh_alta.n_faces)145 fator = np.clip(fator, 0.10, 0.95) 146 mesh_proxy = mesh_alta.decimate(fator)147 else: mesh_proxy = mesh_alta148 except: mesh_proxy = mesh_alta 149 mesh_proxy.save(path_proxy)150 mesh_proxy_visual = mesh_proxy.copy()151 mesh_proxy_visual.points /= 1000.0152 with tempfile.NamedTemporaryFile(suffix=".glb", delete=False) as tmp:153 plotter = pv.Plotter(off_screen=True)154 plotter.add_mesh(mesh_proxy_visual, color="#cccccc", lighting=False)155 plotter.export_gltf(tmp.name)156 with open(tmp.name, "rb") as f: b64_final = base64.b64encode(f.read()).decode()157 prog_bar.empty() 158 return path_stl, path_proxy, vol_mm3, b64_final159 160def gerar_diagnostico_ia(mod_cm, t_sol_total, t_vaz, t_molde, max_risco_rechupe, reynolds, velocidade):161 relatorio = []162 if reynolds > 5000: relatorio.append(f"🌪️ **Turbulência Crítica (Reynolds = {reynolds:.0f}):** Risco de aprisionamento de gás.")163 elif reynolds > 2000: relatorio.append(f"🌊 **Fluxo Transiente (Reynolds = {reynolds:.0f}):** Velocidade típica para coquilhas.")164 else: relatorio.append(f"🐢 **Fluxo Laminar (Reynolds = {reynolds:.0f}):** Escoamento limpo e seguro.")165 if max_risco_rechupe > 0.8: relatorio.append("🚨 **ALERTA DE RECHUPE:** Estrangulamento severo nos cantos internos detectado. Ajuste o derramador.")166 else: relatorio.append("✅ **Alimentação Saudável:** Gradiente térmico direcional preservado.")167 return "\n\n".join(relatorio)168 169# --- GERADOR DE TEMPLATE DE LOADING ---170def gerar_html_viewer(b64_data, cor_destaque, texto_loading, hotspot=None):171 hotspot_html = ""172 if hotspot:173 hotspot_html = f'<button slot="hotspot-1" data-position="{hotspot[0]} {hotspot[1]} {hotspot[2]}" style="background-color: #00BFFF; width: 14px; height: 14px; border-radius: 50%; box-shadow: 0 0 10px #00BFFF; border: 2px solid white;"></button>'174 175 return f"""176 <html>177 <head>178 <script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.4.0/model-viewer.min.js"></script>179 <style>180 body {{ margin: 0; background-color: #1e1e1e; overflow: hidden; font-family: sans-serif; position: relative; }}181 #loader {{ position: absolute; top:0; left:0; width:100%; height:100%; background:#1e1e1e; color:{cor_destaque}; display:flex; align-items:center; justify-content:center; flex-direction:column; z-index:10; transition: opacity 0.5s ease-out; }}182 .spinner {{ border: 4px solid #333; border-top: 4px solid {cor_destaque}; border-radius: 50%; width: 36px; height: 36px; animation: spin 1s linear infinite; margin-bottom: 12px; }}183 @keyframes spin {{ 0% {{ transform: rotate(0deg); }} 100% {{ transform: rotate(360deg); }} }}184 model-viewer {{ width: 100%; height: 320px; }}185 </style>186 </head>187 <body>188 <div id="loader">189 <div class="spinner"></div>190 <div style="font-weight: bold; font-size: 13px; text-align: center;">{texto_loading}</div>191 </div>192 <model-viewer id="mv" src="data:model/gltf-binary;base64,{b64_data}" camera-controls shadow-intensity="1">193 {hotspot_html}194 </model-viewer>195 <script>196 document.getElementById('mv').addEventListener('load', () => {{197 const loader = document.getElementById('loader');198 loader.style.opacity = '0';199 setTimeout(() => loader.style.display = 'none', 500);200 }});201 </script>202 </body>203 </html>204 """205 206# --- INTERFACE ---207if st.session_state.step in [1, 2, 3, 4, 4.5, 5]:208 st.progress(min(st.session_state.step, 4) / 4)209 210if st.session_state.step not in [3.5, 4.5]:211 st.title("🛡️ CastingSim V49")212 st.markdown("---")213 214# ==========================================215# ETAPAS 1 e 2216# ==========================================217if st.session_state.step == 1:218 st.header("1. Refino Metrológico (Controle de Malha)")219 c1, c2, c3 = st.columns(3)220 with c1: st.session_state.lin_def = st.number_input("Deflexão Linear (mm)", 0.0005, 1.0, value=st.session_state.lin_def, format="%.4f", step=0.0001)221 with c2: st.session_state.ang_def = st.slider("Deflexão Angular (Rad)", 0.05, 1.0, value=st.session_state.ang_def)222 with c3: st.session_state.sub_div = st.slider("Refino Solver (Subdivisão)", 0, 3, value=st.session_state.sub_div)223 if st.button("Avançar para Parâmetros ➡️", type="primary"): st.session_state.step = 2; st.rerun()224 225elif st.session_state.step == 2:226 st.header("2. Termodinâmica e Mecânica dos Fluidos")227 c_molde1, c_molde2, c_molde3 = st.columns(3)228 with c_molde1:229 st.session_state.mat_molde = st.selectbox("Material da Coquilha", ["Aço 1045", "Ferro Nodular"], index=0)230 st.session_state.t_molde = st.number_input("Temp. Coquilha (°C)", value=st.session_state.t_molde)231 with c_molde2:232 st.session_state.tinta_ceramica = st.checkbox("Uso de Tinta Cerâmica", value=st.session_state.tinta_ceramica)233 with c_molde3:234 if st.session_state.tinta_ceramica:235 st.session_state.espessura_tinta = st.number_input("Espessura Tinta (mm)", value=st.session_state.espessura_tinta, step=0.5)236 st.markdown("---")237 c_fluido1, c_fluido2, c_fluido3 = st.columns(3)238 with c_fluido1: st.session_state.liga = st.selectbox("Liga de Alumínio", ["SAE 305", "SAE 306", "SAE 329"], index=0)239 with c_fluido2: st.session_state.t_vaz = st.number_input("Temp. Vazamento (°C)", value=st.session_state.t_vaz)240 with c_fluido3: 241 st.session_state.vel_vaz = st.number_input("Vel. Vazamento (m/s)", value=st.session_state.vel_vaz, step=0.1)242 st.session_state.diam_vaz = st.number_input("Ø Derramador (mm)", value=st.session_state.diam_vaz, step=1.0) 243 c_b1, c_b2 = st.columns([1, 8])244 with c_b1:245 if st.button("⬅️ Voltar"): st.session_state.step = 1; st.rerun()246 with c_b2:247 if st.button("Avançar para Geometria ➡️", type="primary"): st.session_state.step = 3; st.rerun()248 249# ==========================================250# ETAPA 3251# ==========================================252elif st.session_state.step == 3:253 st.header("3. Inspeção e Ponto de Vazamento")254 arquivo = st.file_uploader("Upload STEP", type=['step', 'stp'])255 256 if arquivo:257 file_bytes = arquivo.getvalue()258 current_hash = gerar_hash(file_bytes, st.session_state.lin_def, st.session_state.ang_def)259 260 if st.session_state.cache_hash != current_hash:261 path_stl, path_proxy_stl, vol_mm3, b64_final = processar_malha_com_telemetria(file_bytes, st.session_state.lin_def, st.session_state.ang_def)262 st.session_state.cache_hash = current_hash263 st.session_state.path_stl = path_stl264 st.session_state.path_proxy_stl = path_proxy_stl265 st.session_state.b64_proxy = b64_final 266 st.session_state.vol_mm3 = vol_mm3267 st.session_state.coord_iniciada = False 268 269 if not st.session_state.coord_iniciada:270 mesh_leitura = pv.read(st.session_state.path_proxy_stl)271 c = mesh_leitura.center272 b = mesh_leitura.bounds273 st.session_state.off_x, st.session_state.off_y, st.session_state.off_z = float(c[0]), float(c[1]), float(b[5])274 st.session_state.coord_iniciada = True275 276 col_form, col_view = st.columns([1, 2])277 278 with col_form:279 st.markdown("---")280 st.write("**Controle de Gravidade Visual Absoluta**")281 st.caption("Selecione o eixo que aponta para CIMA na sua visualização atual.")282 283 eixo_sel = st.selectbox(284 "Eixo Vertical (Gravidade Invertida)", 285 ["X", "Y", "Z"], 286 index=["X", "Y", "Z"].index(st.session_state.eixo_vaz),287 key="eixo_sel_key",288 on_change=atualizar_eixo289 )290 st.markdown("---")291 rx = st.slider("Rotação Visual X (Tombo)", 0, 360, value=int(st.session_state.rot_x))292 ry = st.slider("Rotação Visual Y (Tombo)", 0, 360, value=int(st.session_state.rot_y))293 rz = st.slider("Rotação Visual Z (Tombo)", 0, 360, value=int(st.session_state.rot_z))294 st.write("**Centro Local do Furo (mm)**")295 ox = st.number_input("Eixo X", value=float(st.session_state.off_x), step=1.0)296 oy = st.number_input("Eixo Y", value=float(st.session_state.off_y), step=1.0)297 oz = st.number_input("Eixo Z", value=float(st.session_state.off_z), step=1.0)298 299 # A MÁGICA DA V49: Atualização direta das variáveis do Streamlit300 st.session_state.rot_x, st.session_state.rot_y, st.session_state.rot_z = rx, ry, rz301 st.session_state.off_x, st.session_state.off_y, st.session_state.off_z = ox, oy, oz302 303 # Botão agora é um st.button clássico304 if st.button("🔄 Atualizar Visualização"):305 st.rerun()306 307 with col_view:308 p_m = [st.session_state.off_x/1000.0, st.session_state.off_y/1000.0, st.session_state.off_z/1000.0]309 html = f"""310 <script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.4.0/model-viewer.min.js"></script>311 <model-viewer src="data:model/gltf-binary;base64,{st.session_state.b64_proxy}" 312 orientation="{st.session_state.rot_x}deg {st.session_state.rot_y}deg {st.session_state.rot_z}deg"313 style="width: 100%; height: 480px; background-color: #f4f6f9;" camera-controls>314 <button slot="hotspot-1" data-position="{p_m[0]} {p_m[1]} {p_m[2]}" style="background-color: #00BFFF; width: 16px; height: 16px; border-radius: 50%; box-shadow: 0 0 12px #00BFFF; border: 2px solid white;"></button>315 </model-viewer>316 """317 components.html(html, height=500)318 319 c_b1, c_b2 = st.columns([1, 8])320 with c_b1:321 if st.button("⬅️ Voltar"): st.session_state.step = 2; st.rerun()322 with c_b2:323 if arquivo:324 if st.button("🔥 RODAR SOLVER FEM ➡️", type="primary"): 325 st.session_state.step = 3.5; st.rerun()326 327# ==========================================328# ETAPA 3.5: SOLVER FÍSICO E MATRIZ329# ==========================================330elif st.session_state.step == 3.5:331 st.title("⏳ Processando Matriz Euleriana & Fusão de Malha...")332 progress_bar = st.progress(0, text="Calculando Rotações Absolutas...")333 334 try:335 mesh_pesada = pv.read(st.session_state.path_stl)336 if st.session_state.sub_div > 0:337 mesh_pesada = mesh_pesada.subdivide(st.session_state.sub_div, subfilter='linear')338 339 mesh_pesada.rotate_x(st.session_state.rot_x, inplace=True)340 mesh_pesada.rotate_y(st.session_state.rot_y, inplace=True)341 mesh_pesada.rotate_z(st.session_state.rot_z, inplace=True)342 343 ponto_matriz = pv.PolyData([[st.session_state.off_x, st.session_state.off_y, st.session_state.off_z]])344 ponto_matriz.rotate_x(st.session_state.rot_x, inplace=True)345 ponto_matriz.rotate_y(st.session_state.rot_y, inplace=True)346 ponto_matriz.rotate_z(st.session_state.rot_z, inplace=True)347 px_rot, py_rot, pz_rot = ponto_matriz.points[0]348 st.session_state.p_riser_real = [px_rot, py_rot, pz_rot]349 350 R_vaz = st.session_state.diam_vaz / 2.0351 eixo = st.session_state.eixo_vaz352 bounds = mesh_pesada.bounds353 354 if eixo == 'Y':355 dir_vec = (0, 1, 0)356 topo = bounds[3] + 50.0 357 base = py_rot 358 altura = abs(topo - base)359 center = (px_rot, (topo + base)/2.0, pz_rot)360 elif eixo == 'Z':361 dir_vec = (0, 0, 1)362 topo = bounds[5] + 50.0363 base = pz_rot364 altura = abs(topo - base)365 center = (px_rot, py_rot, (topo + base)/2.0)366 elif eixo == 'X':367 dir_vec = (1, 0, 0)368 topo = bounds[1] + 50.0369 base = px_rot370 altura = abs(topo - base)371 center = ((topo + base)/2.0, py_rot, pz_rot)372 373 cilindro = pv.Cylinder(center=center, direction=dir_vec, radius=R_vaz, height=altura, resolution=100).triangulate()374 if st.session_state.sub_div > 0:375 cilindro = cilindro.subdivide(st.session_state.sub_div, subfilter='linear')376 377 mesh_unified = mesh_pesada.merge(cilindro)378 379 progress_bar.progress(40, text="Resolvendo Cinemática de Navier-Stokes...")380 pts = mesh_unified.points381 382 if eixo == 'Y': h_anim, r_coords = pts[:, 1], np.sqrt((pts[:, 0] - px_rot)**2 + (pts[:, 2] - pz_rot)**2)383 elif eixo == 'Z': h_anim, r_coords = pts[:, 2], np.sqrt((pts[:, 0] - px_rot)**2 + (pts[:, 1] - py_rot)**2)384 elif eixo == 'X': h_anim, r_coords = pts[:, 0], np.sqrt((pts[:, 1] - py_rot)**2 + (pts[:, 2] - pz_rot)**2)385 386 h_topo = np.max(h_anim)387 h_base = np.min(h_anim)388 H_total = h_topo - h_base389 390 campo = np.zeros(len(pts))391 mask_sprue = r_coords <= R_vaz392 mask_mold = ~mask_sprue393 394 campo[mask_sprue] = h_topo - h_anim[mask_sprue]395 campo[mask_mold] = H_total + (h_anim[mask_mold] - h_base) * 1.5 + (r_coords[mask_mold] - R_vaz) * 0.8396 397 turbulencia = np.random.normal(0, 0.05 * H_total, size=len(pts))398 campo += turbulencia399 campo_norm = (campo - np.min(campo)) / (np.max(campo) - np.min(campo) + 1e-6)400 mesh_unified['Preenchimento'] = campo_norm401 402 calor_dinamico = 1.0 - campo_norm 403 dist_ao_centro = np.linalg.norm(pts - mesh_unified.center, axis=1)404 massa_termica = 1.0 - (dist_ao_centro / (np.max(dist_ao_centro) + 1e-6))405 mesh_unified['MassaTermica'] = massa_termica 406 407 curv_negativa = np.clip(-mesh_unified.curvature(curv_type='mean'), 0, None)408 curv_norm = np.where(curv_negativa > np.percentile(curv_negativa, 85), curv_negativa, 0) / (np.max(curv_negativa) + 1e-6)409 410 p_riser_arr = np.array([px_rot, py_rot, pz_rot])411 isolamento = np.linalg.norm(pts - p_riser_arr, axis=1) / (np.max(np.linalg.norm(pts - p_riser_arr, axis=1)) + 1e-6)412 413 risco_bruto = np.clip((massa_termica * 0.4) + (calor_dinamico * 0.4) + (curv_norm * 0.2), 0, 1)414 mesh_unified['Heat'] = np.log1p(risco_bruto * 40.0) / np.log1p(40.0)415 mesh_unified['Risco'] = np.clip(((massa_termica ** 3) + (curv_norm * 5.0)) * (isolamento ** 2) * 10.0, 0, 1)416 mesh_unified['Solidificacao'] = np.clip((massa_termica * 0.7) + (calor_dinamico * 0.3), 0, 1)417 418 mod_cm = (st.session_state.vol_mm3 / pv.read(st.session_state.path_stl).area) / 10 419 num_reynolds = (st.session_state.vel_vaz * ((mod_cm * 2) / 100.0)) / 1.3e-6420 base_b = 150 if st.session_state.mat_molde == "Aço 1045" else 180421 if st.session_state.tinta_ceramica: base_b += (st.session_state.espessura_tinta * 80) 422 t_sol_segundos_totais = (base_b * (1 + (0.002 * max(st.session_state.t_vaz - 660, 1)))) * (mod_cm ** 2)423 st.session_state.t_sol_minutos = int(t_sol_segundos_totais // 60)424 st.session_state.t_sol_segundos = int(t_sol_segundos_totais % 60)425 st.session_state.relatorio_ia = gerar_diagnostico_ia(mod_cm, t_sol_segundos_totais, st.session_state.t_vaz, st.session_state.t_molde, float(np.max(mesh_unified['Risco'])), num_reynolds, st.session_state.vel_vaz)426 st.session_state.relatorio_pdf = compilar_pdf()427 428 progress_bar.progress(70, text="Forjando Proxy Visual com Baixa Latência...")429 mesh_proxy = pv.read(st.session_state.path_proxy_stl)430 mesh_proxy.rotate_x(st.session_state.rot_x, inplace=True)431 mesh_proxy.rotate_y(st.session_state.rot_y, inplace=True)432 mesh_proxy.rotate_z(st.session_state.rot_z, inplace=True)433 434 proxy_cilindro = pv.Cylinder(center=center, direction=dir_vec, radius=R_vaz, height=altura, resolution=50).triangulate()435 proxy_unified = mesh_proxy.merge(proxy_cilindro)436 437 proxy_resultados = proxy_unified.sample(mesh_unified)438 temp_dir = tempfile.mkdtemp()439 path_resultados = os.path.join(temp_dir, "resultados_proxy.vtk")440 proxy_resultados.save(path_resultados)441 st.session_state.path_resultados_proxy = path_resultados442 443 progress_bar.progress(85, text="Gerando Hologramas Estáticos...")444 m_heat = proxy_resultados.copy(); m_heat.points /= 1000.0445 with tempfile.NamedTemporaryFile(suffix=".glb", delete=False) as tmp:446 pl = pv.Plotter(off_screen=True); pl.add_mesh(m_heat, scalars='Heat', cmap="hot", smooth_shading=True)447 pl.export_gltf(tmp.name)448 with open(tmp.name, "rb") as f: st.session_state.b64_termico = base64.b64encode(f.read()).decode()449 450 m_solid = proxy_resultados.copy(); m_solid.points /= 1000.0451 with tempfile.NamedTemporaryFile(suffix=".glb", delete=False) as tmp:452 pl = pv.Plotter(off_screen=True); pl.add_mesh(m_solid, scalars='Solidificacao', cmap="turbo", smooth_shading=True)453 pl.export_gltf(tmp.name)454 with open(tmp.name, "rb") as f: st.session_state.b64_solidificacao = base64.b64encode(f.read()).decode()455 456 m_rec = mesh_unified.copy(); m_rec.compute_normals(inplace=True); m_def = m_rec.warp_by_scalar(scalars='Risco', factor=-10.0); m_def.points /= 1000.0457 with tempfile.NamedTemporaryFile(suffix=".glb", delete=False) as tmp:458 pl = pv.Plotter(off_screen=True); pl.add_mesh(m_def, scalars='Risco', cmap="hot", smooth_shading=True)459 pl.export_gltf(tmp.name)460 with open(tmp.name, "rb") as f: st.session_state.b64_rechupe_x10 = base64.b64encode(f.read()).decode()461 462 st.session_state.step = 4463 st.rerun()464 465 except Exception as e:466 st.error(f"💥 FALHA MATEMÁTICA: {e}")467 st.code(traceback.format_exc())468 if st.button("⬅️ Voltar"): st.session_state.step = 3; st.rerun()469 470# ==========================================471# ETAPA 4: LAUDO (COM HOLOGRAMA DE LOADING V48)472# ==========================================473elif st.session_state.step == 4:474 475 tela4_container = st.empty()476 477 with tela4_container.container():478 st.header("4. Simulação")479 480 col_tempo, col_video = st.columns([3, 1])481 with col_tempo:482 st.error(f"⏱️ **TEMPO CRÍTICO DE SOLIDIFICAÇÃO: {st.session_state.t_sol_minutos} min {st.session_state.t_sol_segundos} s**")483 with col_video:484 if st.button("🎬 Painel de Extração Temporal", help="Abre o cinema", use_container_width=True):485 tela4_container.empty()486 time.sleep(0.2)487 st.session_state.step = 4.5488 st.rerun()489 490 col_v1, col_v2, col_v3 = st.columns(3)491 p_m = [st.session_state.p_riser_real[0]/1000.0, st.session_state.p_riser_real[1]/1000.0, st.session_state.p_riser_real[2]/1000.0]492 493 with col_v1:494 st.write("🔥 **1. Preenchimento Térmico**")495 html_termico = gerar_html_viewer(st.session_state.b64_termico, "#ff4500", "Renderizando Mapa Térmico...", hotspot=p_m)496 components.html(html_termico, height=360)497 498 with col_v2:499 st.write("🌈 **2. Mapa de Solidificação**")500 st.caption("Frio (Azul) = Congela Rápido | Quente (Vermelho) = Lento")501 html_solid = gerar_html_viewer(st.session_state.b64_solidificacao, "#00BFFF", "Calculando Gradientes...")502 components.html(html_solid, height=360)503 504 with col_v3:505 st.write("⚠️ **3. Rechupe (Deformação x10)**")506 html_rechupe = gerar_html_viewer(st.session_state.b64_rechupe_x10, "#ffcc00", "Montando Malha de Alta Resolução...<br><span style='font-size: 11px; color: #888;'>Isso leva alguns segundos</span>")507 components.html(html_rechupe, height=360)508 509 st.markdown("---")510 c_ia, c_btn = st.columns([3, 1])511 with c_ia:512 st.subheader("🤖 Diagnóstico IA")513 st.markdown(st.session_state.relatorio_ia)514 with c_btn:515 st.write("")516 if HAS_FPDF and st.session_state.relatorio_pdf:517 st.download_button("📄 Baixar Laudo (.PDF)", st.session_state.relatorio_pdf, "Laudo_CastingSim.pdf", "application/pdf", type="primary", use_container_width=True)518 if st.button("⬅️ Voltar para Geometria", use_container_width=True): 519 tela4_container.empty()520 time.sleep(0.1)521 st.session_state.step = 3; st.rerun()522 523# ==========================================524# ETAPA 4.5: EXTRAÇÃO TEMPORAL525# ==========================================526elif st.session_state.step == 4.5:527 gc.collect() 528 st.title("🎬 Renderizando Motor Temporal")529 prog = st.progress(0)530 531 try:532 mesh_animacao = pv.read(st.session_state.path_resultados_proxy)533 534 try:535 mold_ghost = mesh_animacao.decimate(0.95)536 except:537 mold_ghost = mesh_animacao.outline() 538 539 mold_ghost.points /= 1000.0540 541 st.session_state.frames_filling = []542 st.session_state.frames_cooling = []543 temp_dir = tempfile.mkdtemp()544 545 passos = 10546 for i in range(1, passos + 1):547 lim = (1.0 / passos) * i548 if lim <= 0: lim = 1e-6 549 550 f_mesh = mesh_animacao.threshold([0, lim], scalars='Preenchimento')551 552 pl = pv.Plotter(off_screen=True)553 pl.add_mesh(mold_ghost, color='white', opacity=0.1, style='wireframe')554 555 if f_mesh.n_points > 0:556 f_mesh.points /= 1000.0557 pl.add_mesh(f_mesh, color='#ff4500', metallic=True, smooth_shading=True)558 559 path = os.path.join(temp_dir, f"fill_{i}.glb")560 pl.export_gltf(path)561 st.session_state.frames_filling.append(path)562 pl.close()563 prog.progress(int((i/passos)*40), text="[1/2] Forjando Animação de Queda Absoluta...")564 565 t_v = st.session_state.t_vaz566 t_m = st.session_state.t_molde567 massa = mesh_animacao['MassaTermica']568 tau = (massa + 0.1) * (st.session_state.t_sol_minutos * 60 + st.session_state.t_sol_segundos + 1) / 3.0 569 570 for i in range(passos):571 t_instante = (i / (passos - 1)) * (st.session_state.t_sol_minutos * 60 + st.session_state.t_sol_segundos + 1)572 T_t = t_m + (t_v - t_m) * np.exp(-t_instante / tau)573 heat_norm = (T_t - t_m) / (t_v - t_m + 1e-6)574 575 m_cool = mesh_animacao.copy()576 m_cool['TempAtual'] = heat_norm577 m_cool.points /= 1000.0578 579 pl = pv.Plotter(off_screen=True)580 pl.add_mesh(m_cool, scalars='TempAtual', cmap="turbo", smooth_shading=True, clim=[0, 1])581 path = os.path.join(temp_dir, f"cool_{i}.glb")582 pl.export_gltf(path)583 st.session_state.frames_cooling.append(path)584 pl.close()585 prog.progress(40 + int(((i+1)/passos)*60), text="[2/2] Extraindo Frames Termodinâmicos...")586 587 prog.progress(100, text="Processamento finalizado!")588 time.sleep(0.5)589 st.session_state.step = 5590 st.rerun()591 592 except Exception as e:593 st.error(f"Erro na extração de vídeo: {e}")594 if st.button("Voltar"): st.session_state.step = 4; st.rerun()595 596# ==========================================597# ETAPA 5: O CINEMA598# ==========================================599elif st.session_state.step == 5:600 st.header("5. Telemetria Cinemática")601 602 modo = st.radio("Selecione o Módulo de Visualização:", ["🌊 Cinemática de Preenchimento", "❄️ Termodinâmica de Resfriamento (Fourier)"], horizontal=True)603 caminhos_alvo = st.session_state.frames_filling if "Preenchimento" in modo else st.session_state.frames_cooling604 605 if len(caminhos_alvo) > 0:606 frames_b64 = []607 for p in caminhos_alvo:608 try:609 with open(p, "rb") as f: frames_b64.append(base64.b64encode(f.read()).decode())610 except: pass611 612 frames_js_array = ",\n".join([f'"{b64}"' for b64 in frames_b64])613 css_thumb = "#ff4500" if "Preenchimento" in modo else "#00BFFF"614 615 html_player = f"""616 <html>617 <head>618 <script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.4.0/model-viewer.min.js"></script>619 <style>620 body {{ margin: 0; background-color: #f4f6f9; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; overflow: hidden; font-family: sans-serif; }}621 model-viewer {{ width: 100%; height: 85%; background-color: #1e1e1e; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.3); }}622 .controls {{ height: 15%; display: flex; align-items: center; justify-content: center; width: 100%; gap: 15px; padding: 10px; box-sizing: border-box; }}623 input[type=range] {{ -webkit-appearance: none; width: 60%; background: transparent; }}624 input[type=range]::-webkit-slider-thumb {{ -webkit-appearance: none; height: 24px; width: 24px; border-radius: 50%; background: {css_thumb}; cursor: pointer; margin-top: -8px; box-shadow: 0 0 8px {css_thumb}; }}625 input[type=range]::-webkit-slider-runnable-track {{ width: 100%; height: 8px; cursor: pointer; background: #cccccc; border-radius: 4px; }}626 .status {{ color: #1e1e1e; font-weight: bold; font-size: 16px; background-color: white; padding: 5px 15px; border-radius: 20px; border: 1px solid #ccc; }}627 </style>628 </head>629 <body>630 <model-viewer id="mv" src="data:model/gltf-binary;base64,{frames_b64[0]}" camera-controls shadow-intensity="1"></model-viewer>631 <div class="controls">632 <span style="font-weight: bold;">Início</span>633 <input type="range" min="1" max="{len(frames_b64)}" value="1" class="slider" id="frameSlider">634 <span style="font-weight: bold;">Fim</span>635 <div class="status" id="frame-counter">Quadro: 1 / {len(frames_b64)}</div>636 </div>637 <script>638 const frames = [{frames_js_array}];639 const mv = document.getElementById('mv');640 const slider = document.getElementById('frameSlider');641 const counter = document.getElementById('frame-counter');642 643 slider.oninput = function() {{644 let idx = this.value - 1;645 mv.src = "data:model/gltf-binary;base64," + frames[idx];646 counter.innerText = "Quadro: " + this.value + " / " + frames.length;647 }}648 </script>649 </body>650 </html>651 """652 components.html(html_player, height=550)653 else:654 st.warning("Não há frames disponíveis.")655 656 st.write("")657 if st.button("⬅️ Voltar ao Laudo Principal (Etapa 4)", type="primary"):658 st.session_state.step = 4; st.rerun()