CoolFace
Apppublic

andrefbfreitas/casting-sim

sourceHugging Faceotherupdated 7mo agoView on Hugging Face
0likes
app.py658 linesDownload Raw Back to root
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()