diff --git a/__pycache__/vr4life_cloud.cpython-311.pyc b/__pycache__/vr4life_cloud.cpython-311.pyc index 2695c6d..b41b95b 100644 Binary files a/__pycache__/vr4life_cloud.cpython-311.pyc and b/__pycache__/vr4life_cloud.cpython-311.pyc differ diff --git a/__pycache__/vr4life_engine.cpython-311.pyc b/__pycache__/vr4life_engine.cpython-311.pyc index f2b3e19..b3b5bb8 100644 Binary files a/__pycache__/vr4life_engine.cpython-311.pyc and b/__pycache__/vr4life_engine.cpython-311.pyc differ diff --git a/__pycache__/vr4life_ui.cpython-311.pyc b/__pycache__/vr4life_ui.cpython-311.pyc index 12f2860..7b246b8 100644 Binary files a/__pycache__/vr4life_ui.cpython-311.pyc and b/__pycache__/vr4life_ui.cpython-311.pyc differ diff --git a/run_vr4life.py b/run_vr4life.py index 0ebde46..9f7fef8 100644 --- a/run_vr4life.py +++ b/run_vr4life.py @@ -12,6 +12,10 @@ importlib.reload(vr4life_ui) if __name__ == "__main__": from pymxs import runtime as rt - app = vr4life_ui.AutoBakeManager() - rt.vr4_app = app - app.show() \ No newline at end of file + + # Previne que o Garbage Collector limpe a interface durante o Render pesado + global _vr4life_global_window + _vr4life_global_window = vr4life_ui.AutoBakeManager() + + rt.vr4_app = _vr4life_global_window + _vr4life_global_window.show() \ No newline at end of file diff --git a/vr4_config.json b/vr4_config.json new file mode 100644 index 0000000..601cbb0 --- /dev/null +++ b/vr4_config.json @@ -0,0 +1 @@ +{"api_hash": "c863105dd63bd3c46f24c3d0f42f44ec9a6348bf6543b76911c32ea73350d083815b8daf2df54e9473f49f57c788ee4af350c9c970ea7f737e517a510188c0e1"} \ No newline at end of file diff --git a/vr4_state.json b/vr4_state.json index 034fa49..0637a08 100644 --- a/vr4_state.json +++ b/vr4_state.json @@ -1 +1 @@ -[{"name": "3d66-Editable_Poly-23107237-414", "status": "Pronto", "polys": "27,680", "res": "2048px", "id": "979.0"}, {"name": "3d66-Editable_Poly-23107237-415", "status": "Pronto", "polys": "15,776", "res": "2048px", "id": "797.2"}, {"name": "3d66diban-020", "status": "Pronto", "polys": "600", "res": "2048px", "id": "861.0"}] \ No newline at end of file +[] \ No newline at end of file diff --git a/vr4life_cloud.py b/vr4life_cloud.py index db41d9d..10dde98 100644 --- a/vr4life_cloud.py +++ b/vr4life_cloud.py @@ -3,122 +3,255 @@ from pymxs import runtime as rt try: from PySide6 import QtWidgets except ImportError: from PySide2 import QtWidgets +import urllib.request +import urllib.parse +import json + def mock_connect_api(ui): - if len(ui.e_hash.text()) < 5: QtWidgets.QMessageBox.warning(ui, "Acesso Negado", "Hash inválida."); return - ui.b_conn.setText("Conectando..."); QtWidgets.QApplication.processEvents(); time.sleep(1) - for w in [ui.c_chn, ui.c_pst, ui.e_tit, ui.e_mp3, ui.b_mp3]: w.setEnabled(True); w.setStyleSheet("background: white; color: black; font-weight: bold;") - ui.c_chn.clear(); ui.c_chn.addItems(["Canal: Imóveis Virtuais", "Canal: Showroom Zombisco"]) - ui.c_pst.clear(); ui.c_pst.addItems(["[ + Cadastrar Nova Postagem ]", "Editar: Apartamento V1"]) - ui.chk_up.setEnabled(True); ui.chk_up.setStyleSheet("color: #FFD700; font-weight: bold;"); ui.chk_up.setChecked(True) - ui.b_conn.setText("✅ Conectado!"); ui.b_conn.setStyleSheet("background: #000; color: #0F0; font-weight: bold; border: 1px solid #0F0;") - on_post_changed(ui, 0) + hash_txt = ui.e_hash.text().strip() + if len(hash_txt) < 5: + QtWidgets.QMessageBox.warning(ui, "Acesso Negado", "Hash inválida.") + return + + ui.b_conn.setText("Conectando..."); QtWidgets.QApplication.processEvents() + + # Faz chamada real para a API + url = "https://api.vr4life.com/public/listaCanais" + req_data = urllib.parse.urlencode({"tx_hash_login_externo": hash_txt}).encode('utf-8') + req = urllib.request.Request(url, data=req_data, headers={'Content-Type': 'application/x-www-form-urlencoded'}) + + try: + with urllib.request.urlopen(req, timeout=10) as response: + res_body = response.read().decode('utf-8') + canais = json.loads(res_body) + + ui.c_chn.clear() + # Espera receber lista: [{"nm_canal": "...", "tx_hash_canal": "..."}, ...] + if isinstance(canais, list) and len(canais) > 0: + for c in canais: + n_c = str(c.get("nm_canal", "Canal Desconhecido")) + h_c = str(c.get("tx_hash_canal", "")) + ui.c_chn.addItem(n_c, h_c) + else: + raise Exception("Nenhum canal encontrado para este Hash.") + + except Exception as e: + ui.b_conn.setText("🔄 Tentar Novamente") + QtWidgets.QMessageBox.critical(ui, "Erro de API", f"Falha ao conectar: {str(e)}") + return -def on_post_changed(ui, idx): - if idx == 0: - ui.e_tit.setText(""); ui.e_tit.setPlaceholderText("Título do projeto...") - ui.chk_tmb.setChecked(True); ui.chk_tmb.setEnabled(False); ui.chk_tmb.setStyleSheet("color: #00FF00; font-weight: bold;") - else: - ui.e_tit.setText(ui.c_pst.currentText().replace("Editar: ", "")) - ui.chk_tmb.setEnabled(True); ui.chk_tmb.setStyleSheet("color: white; font-weight: bold;") + # Ativa campos após sucesso + for w in [ui.c_chn, ui.rdo_add, ui.rdo_edt, ui.e_tit, ui.e_mp3, ui.b_mp3]: + w.setEnabled(True) + w.setStyleSheet("background: white; color: black; font-weight: bold;") + + ui.b_up.setEnabled(True) + ui.b_conn.setText("✅ Conectado!"); ui.b_conn.setStyleSheet("background: #000; color: #0F0; font-weight: bold; border: 1px solid #0F0;") + + # Auto-carrega postagens do primeiro canal + fetch_posts_for_channel(ui) -def finalize_export(ui, p_bk, p_glb): - if not os.path.exists(p_glb): os.makedirs(p_glb) - fo = []; rt.execute("max modify mode"); tgs = ui.get_processable_items() +def fetch_posts_for_channel(ui): + ui.c_pst.blockSignals(True) + ui.c_pst.clear() - ui.pb.setFormat("Preparando materiais VR (Multi-ID)..."); QtWidgets.QApplication.processEvents() + hash_txt = ui.e_hash.text().strip() + idx = ui.c_chn.currentIndex() + has_posts = False - for d in tgs: - if ui._is_cancelled: break - if "Bake" in d['item'].text(1) or "Já existe" in d['item'].text(1) or "Mat" in d['item'].text(1): + if idx >= 0: + ch_hash = ui.c_chn.itemData(idx) + print(f"[DEBUG UI] Canal Index {idx}. Valor Extraído do ItemData: '{ch_hash}'") + if ch_hash and str(ch_hash).strip(): + ch_hash = str(ch_hash).strip() + url = "https://api.vr4life.com/public/listaPostagemCanal" + dict_data = {"tx_hash_login_externo": hash_txt, "tx_hash_canal": ch_hash} + req_data = urllib.parse.urlencode(dict_data).encode('utf-8') + + print(f"\n[DEBUG API] Chamando: {url}") + print(f"[DEBUG API] Payload (FormData): {dict_data}") + + req = urllib.request.Request(url, data=req_data, headers={'Content-Type': 'application/x-www-form-urlencoded'}) + try: - max_id = rt.execute(f"""(local o = getNodeByName "{d['name']}"; local mid = 1; if o != undefined and isKindOf o Editable_Poly do ( for f = 1 to (polyop.getNumFaces o) do ( local id = polyop.getFaceMatID o f; if id > mid do mid = id ) ); mid)""") - - if max_id > 1: - # CONSTRUTOR DE MULTI-MATERIAL (Para objetos que foram processados pelo P4: Multi-UV) - ms_mat = f"""( - local o = getNodeByName "{d['name']}" - local mm = MultiMaterial numsubs:{int(max_id)} name:(o.name + "_MultiMat") - for i = 1 to {int(max_id)} do ( - local ip = @"{p_bk}/{d['name']}_B_ID" + (i as string) + ".jpg" - if doesFileExist ip do ( - local m = PhysicalMaterial name:(o.name + "_MatID_" + (i as string)) - m.base_color_map = BitmapTexture filename:ip - m.roughness = 1.0; m.metalness = 0.0; try(m.reflectivity = 0.0; m.reflection_weight = 0.0)catch() - mm[i] = m - ) - ) - o.material = mm - )""" - rt.execute(ms_mat) - d['item'].setText(1, "Mat OK (Multi)") - fo.append(rt.getNodeByName(d['name'])) - - else: - # MATERIAL PADRÃO (Para objetos normais que cabem numa textura só) - ip = os.path.join(p_bk, f"{d['name']}_B.jpg").replace("\\", "/") - if os.path.exists(ip): - rt.execute(f"""( - local o = getNodeByName "{d['name']}" - if o != undefined do ( - local m = PhysicalMaterial name:(o.name + "_Mat"); m.base_color_map = BitmapTexture filename:@"{ip}" - m.roughness = 1.0; m.metalness = 0.0; try(m.reflectivity = 0.0; m.reflection_weight = 0.0)catch() - o.material = m - ) - )""") - fo.append(rt.getNodeByName(d['name'])); d['item'].setText(1, "Mat OK") + with urllib.request.urlopen(req, timeout=10) as response: + raw_res = response.read().decode('utf-8') + posts = json.loads(raw_res) + if isinstance(posts, list) and len(posts) > 0: + has_posts = True + for p in posts: + t_p = p.get('tx_titulo', 'Sem Título') + h_p = p.get('hash_postagem', '') + print(f"[DEBUG API] Encontrou Postagem: '{t_p}' | Hash: {h_p}") + ui.c_pst.addItem(t_p, h_p) + except urllib.error.HTTPError as e: + err_body = e.read().decode('utf-8') + QtWidgets.QMessageBox.critical(ui, f"Erro HTTP {e.code}", f"O Servidor RECUSOU a requisição.\n\nResposta:\n{err_body}") except Exception as e: - d['item'].setText(1, "Erro Mat"); print(f"Erro Material em {d['name']}: {e}") + QtWidgets.QMessageBox.warning(ui, "Aviso de Rede", f"Falha na comunicação ou falha lendo o JSON de postagens:\n\n{e}\n\n(A API pode ter retornado HTML ao invés de JSON puro?)") + else: + QtWidgets.QMessageBox.warning(ui, "Aviso", "O Hash deste canal não foi preenchido.") + + ui.c_pst.blockSignals(False) + + # Se não retornou posts, forçar modo "Novo" e bloquear edição + if not has_posts: + ui.rdo_add.setChecked(True) + ui.rdo_edt.setEnabled(False) + else: + ui.rdo_edt.setEnabled(True) + + on_post_changed(ui) + +def on_post_changed(ui): + if ui.rdo_add.isChecked(): + ui.c_pst.setEnabled(False) + ui.c_pst.setStyleSheet("background: #CCC; color: black; font-weight: normal;") + ui.e_tit.setText(""); ui.e_tit.setPlaceholderText("Título do NOVO projeto...") + ui.b_tmb.setEnabled(False) + else: + ui.c_pst.setEnabled(True) + ui.c_pst.setStyleSheet("background: white; color: black; font-weight: bold;") + if ui.c_pst.count() > 0: + ui.e_tit.setText(ui.c_pst.currentText()) + ui.b_tmb.setEnabled(True) + +def finalize_export(ui, p_glb, f_name): + # Transição de abas após o término do Exportador V19 + ui.tabs.setCurrentIndex(3) + +def generate_thumbnail(ui): + p_glb = ui.edt_p_glb.text().replace("\\", "/") + if not p_glb.endswith("/"): p_glb += "/" + tgs = ui.get_processable_items() + if len(tgs) == 1: sn = f"{tgs[0]['name']}_Export" + else: sn = "VR4Life_Scene_Export" + tp = os.path.join(p_glb, f"{sn}_Thumb.jpg").replace("\\", "/") + + ui.pb.setFormat("📸 Tirando Snapshot da Cena..."); QtWidgets.QApplication.processEvents() + rt.execute(f"""( local c = getActiveCamera(); if c == undefined do ( local ac = for cam in cameras where (classof cam != TargetObject) collect cam; if ac.count > 0 do c = ac[1] ); try ( if c != undefined then ( render camera:c outputwidth:1280 outputheight:720 outputfile:@"{tp}" vfb:false quiet:true ) else ( render outputwidth:1280 outputheight:720 outputfile:@"{tp}" vfb:false quiet:true ) ) catch() )""") + ui.pb.setFormat("✅ Thumbnail Gerado e Salvo!"); ui.pb.setValue(100) + + try: os.startfile(p_glb) + except: pass + +def upload_to_cloud(ui): + p_glb = ui.edt_p_glb.text().replace("\\", "/") + if not p_glb.endswith("/"): p_glb += "/" + + tgs = ui.get_processable_items() + if len(tgs) == 1: sn = f"{tgs[0]['name']}_Export" + else: sn = "VR4Life_Scene_Export" + + oz = os.path.join(p_glb, f"{sn}.zip").replace("\\", "/") + tp = os.path.join(p_glb, f"{sn}_Thumb.jpg").replace("\\", "/") + + if not os.path.exists(oz): + try: + zip_files = [os.path.join(p_glb, f) for f in os.listdir(p_glb) if f.lower().endswith('.zip')] + if zip_files: + oz = max(zip_files, key=os.path.getmtime) + base_zip_name = os.path.basename(oz).replace(".zip", "") + alt_tp = os.path.join(p_glb, f"{base_zip_name}_Thumb.jpg") + if os.path.exists(alt_tp): tp = alt_tp + except Exception as e: print(f"Erro ao buscar zip recente: {e}") - if not fo and not ui._is_cancelled: - QtWidgets.QMessageBox.warning(ui, "Exportação Interrompida", "Nenhum objeto validado para exportação.") + if not os.path.exists(oz): + QtWidgets.QMessageBox.warning(ui, "Aviso", f"Nenhum arquivo ZIP encontrado na pasta:\n{p_glb}\n\nFaça o Bake primeiro!") + return + + ui.pb.setFormat(f"📡 API: Sincronizando com a Nuvem..."); QtWidgets.QApplication.processEvents() + + hx_login = ui.e_hash.text().strip() + ch_idx = ui.c_chn.currentIndex() + hx_canal = ui.c_chn.itemData(ch_idx) if ch_idx >= 0 else "" + titulo = ui.e_tit.text().strip() + hx_postagem = "" + if ui.rdo_edt.isChecked(): + pst_idx = ui.c_pst.currentIndex() + if pst_idx >= 0: hx_postagem = ui.c_pst.itemData(pst_idx) + + if not hx_login or not hx_canal or not titulo: + QtWidgets.QMessageBox.warning(ui, "Campos Inválidos", "Por favor conecte-se à API, selecione um Canal e digite um Título antes de enviar!") ui.pb.setFormat("Pronto"); ui.pb.setValue(0) return - if fo and not ui._is_cancelled: - rt.select(fo); sn = rt.maxFileName.split(".")[0] if rt.maxFileName else "Cena_VR4LIFE" - og = os.path.join(p_glb, f"{sn}.glb").replace("\\", "/"); oz = os.path.join(p_glb, f"{sn}.zip").replace("\\", "/") - tp = os.path.join(p_glb, f"{sn}_Thumb.jpg").replace("\\", "/"); mp = ui.e_mp3.text(); hm = os.path.exists(mp) + import mimetypes + boundary = '----WebKitFormBoundaryVR4PluginX1A' + data_parts = [] + + def add_text_field(name, value): + data_parts.append(f'--{boundary}\r\n'.encode('utf-8')) + data_parts.append(f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode('utf-8')) + data_parts.append(f'{value}\r\n'.encode('utf-8')) - to = False - if ui.chk_tmb.isChecked() and ui.chk_tmb.isEnabled(): - ui.pb.setFormat("📸 Thumbnail (1280x720)..."); QtWidgets.QApplication.processEvents() - rt.execute(f"""( local c = getActiveCamera(); if c == undefined do ( local ac = for cam in cameras where (classof cam != TargetObject) collect cam; if ac.count > 0 do c = ac[1] ); try ( if c != undefined then ( render camera:c outputwidth:1280 outputheight:720 outputfile:@"{tp}" vfb:false quiet:true ) else ( render outputwidth:1280 outputheight:720 outputfile:@"{tp}" vfb:false quiet:true ) ) catch() )""") - if os.path.exists(tp): to = True + def add_file_field(name, filename, filepath): + try: + mime_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream' + with open(filepath, 'rb') as f: + file_data = f.read() + data_parts.append(f'--{boundary}\r\n'.encode('utf-8')) + data_parts.append(f'Content-Disposition: form-data; name="{name}"; filename="{filename}"\r\n'.encode('utf-8')) + data_parts.append(f'Content-Type: {mime_type}\r\n\r\n'.encode('utf-8')) + data_parts.append(file_data) + data_parts.append(b'\r\n') + except Exception as e: + print(f"[DEBUG UPLOAD] Erro ao ler arquivo {filepath}: {e}") - ui.pb.setFormat("Limpando Hierarquia e XForm..."); QtWidgets.QApplication.processEvents() - - rt.execute(""" - ( - local sel = selection as array - for o in sel do ( - o.parent = undefined - ResetXForm o - collapseStack o - if not isKindOf o Editable_Poly do try(convertToPoly o)catch() - ) - ) - """) + add_text_field('tx_hash_login_externo', hx_login) + add_text_field('tx_hash_canal', hx_canal) + add_text_field('tx_titulo', titulo) + if ui.rdo_edt.isChecked() and hx_postagem: + add_text_field('tx_hash_postagem', hx_postagem) - ui.pb.setFormat("Montando GLB..."); QtWidgets.QApplication.processEvents() - try: rt.exportFile(og, rt.name("noPrompt"), selectedOnly=True, using=rt.GLTFExporter) - except: rt.execute(f'exportFile "{og}" #noPrompt selectedOnly:true') - - zo = False - if os.path.exists(og): - ui.pb.setFormat("🗜️ ZIPando GLB..."); QtWidgets.QApplication.processEvents() - try: - with zipfile.ZipFile(oz, 'w', zipfile.ZIP_DEFLATED) as zf: zf.write(og, os.path.basename(og)) - zo = True - except: pass + if not os.path.exists(tp): + ui.pb.setFormat("📸 Tirando Snapshot Automático (Thumbnail)..."); QtWidgets.QApplication.processEvents() + rt.execute(f"""( local c = getActiveCamera(); if c == undefined do ( local ac = for cam in cameras where (classof cam != TargetObject) collect cam; if ac.count > 0 do c = ac[1] ); try ( if c != undefined then ( render camera:c outputwidth:1280 outputheight:720 outputfile:@"{tp}" vfb:false quiet:true ) else ( render outputwidth:1280 outputheight:720 outputfile:@"{tp}" vfb:false quiet:true ) ) catch() )""") - if ui.chk_up.isChecked() and ui.chk_up.isEnabled() and zo: - cs = ui.c_chn.currentText(); ac = "NOVO" if ui.c_pst.currentIndex() == 0 else "ATUALIZAR"; tit = ui.e_tit.text() - ui.pb.setFormat(f"📡 API: Enviando..."); QtWidgets.QApplication.processEvents(); time.sleep(1.5) - msg = f"Sincronizado!\n\n📡 Canal: {cs}\n📝 Ação: {ac}\n🏷️ Titulo: {tit}\n\n📦 ZIP: {os.path.basename(oz)}\n📸 Thumb: {'Sim' if to else 'Não'}\n🎵 Áudio: {'Sim' if hm else 'Não'}" - QtWidgets.QMessageBox.information(ui, "Cloud API", msg) - else: - msg = f"Exportado:\n{og}"; - if zo: msg += f"\n\n🗜️ ZIP:\n{oz}" - if to: msg += f"\n\n📸 Thumb:\n{tp}" - QtWidgets.QMessageBox.information(ui, "Sucesso", msg) + if os.path.exists(tp): + add_file_field('thumbnail', os.path.basename(tp), tp) + + if os.path.exists(oz): + add_file_field('arquivo', os.path.basename(oz), oz) + + data_parts.append(f'--{boundary}--\r\n'.encode('utf-8')) + body = b''.join(data_parts) + + url = "https://api.vr4life.com/public/savePost" + req = urllib.request.Request(url, data=body) + req.add_header('Content-Type', f'multipart/form-data; boundary={boundary}') + req.add_header('Content-Length', str(len(body))) + + print("\n" + "="*50) + print("📡 [DEBUG API UPLOAD] DADOS ENVIADOS NO FORM-DATA:") + print(f" -> tx_hash_login_externo: {hx_login}") + print(f" -> tx_hash_canal : {hx_canal}") + print(f" -> tx_titulo : {titulo}") + + # Se campo não existe, printa vazio pra entender o contexto + if ui.rdo_edt.isChecked() and hx_postagem: + print(f" -> tx_hash_postagem : {hx_postagem}") + + print(f" -> thumbnail (ARQUIVO) : {tp if os.path.exists(tp) else 'NAO ENVIADO'}") + print(f" -> arquivo (ARQUIVO) : {oz if os.path.exists(oz) else 'NAO ENVIADO'}") + print(f"--------------------------------------------------") + print(f"Disparando pacote de {len(body)} bytes para {url} ...") + print("="*50 + "\n") + + ui.b_up.setEnabled(False); ui.b_up.setText("Upload em andamento...") + QtWidgets.QApplication.processEvents() + try: + with urllib.request.urlopen(req, timeout=120) as response: + raw_res = response.read().decode('utf-8') + print(f"[DEBUG API UPLOAD] Sucesso HTTP 200:\n{raw_res}") + QtWidgets.QMessageBox.information(ui, "Sincronização Cloud Concluída", f"O projeto foi enviado com sucesso para a plataforma!\n\nDados Retornados da API:\n{raw_res}") + except urllib.error.HTTPError as e: + err_body = e.read().decode('utf-8') + print(f"[DEBUG API UPLOAD] Erro HTTP {e.code}:\n{err_body}") + QtWidgets.QMessageBox.critical(ui, "Sincronização Falhou", f"O PHP Recusou o Arquivo (HTTP {e.code}).\n\nMotivo:\n{err_body}") + except Exception as e: + print(f"[DEBUG API UPLOAD] Exceção: {e}") + QtWidgets.QMessageBox.warning(ui, "Aviso de Rede", f"Não foi possível completar o envio do arquivo Zip para o servidor.\n\nDetalhes:\n{e}") + finally: + ui.b_up.setEnabled(True); ui.b_up.setText("☁️ Enviar para Nuvem") ui.pb.setFormat("Pronto"); ui.pb.setValue(0) \ No newline at end of file diff --git a/vr4life_engine.py b/vr4life_engine.py index 0ae9cdd..471ec63 100644 --- a/vr4life_engine.py +++ b/vr4life_engine.py @@ -396,6 +396,13 @@ def process_bake_logic_v19(ui, auto_export=False): res_str = d['item'].text(3).replace("px", "") sz = int(res_str) if res_str.isdigit() else res_val + expected_tex = os.path.join(p_bk, f"{obj_name}_Baked.jpg").replace("\\", "/") + if os.path.exists(expected_tex): + mprint(f"--- Ignorando {obj_name} (Ja Bakeado) ---") + d['item'].setText(1, "OK (Salvo)") + d['item'].setForeground(1, QtGui.QColor(0, 255, 0)) + continue + mprint(f"--- Iniciando script V19 portado para: {obj_name} ---") ms_v19_block = f"""( @@ -661,6 +668,27 @@ def export_glb_v19(ui): res = rt.execute(ms_export) if res == True: mprint("✅ SUCESSO: GLB Gerado com Texturas Embutidas (Modo Silencioso)!") + + # O Nome do arquivo destino completo e o GLB gerado: + full_path_glb = os.path.join(p_glb, f_name + ".glb").replace("\\", "/") + full_path_zip = os.path.join(p_glb, f_name + ".zip").replace("\\", "/") + full_path_tmb = os.path.join(p_glb, f_name + "_Thumb.jpg").replace("\\", "/") # Thumbnail adivinhada + + if os.path.exists(full_path_glb): + mprint(f"🗜️ Empacotando GLB em Arquivo ZIP... ({full_path_zip})") + import zipfile + try: + with zipfile.ZipFile(full_path_zip, 'w', zipfile.ZIP_DEFLATED) as zf: + zf.write(full_path_glb, os.path.basename(full_path_glb)) + pass # Se quiséssemos embutir o Thumb no zip, fariamos: if os.path.exists(full_path_tmb): zf.write(full_path_tmb, os.path.basename(full_path_tmb)) + mprint(f"📦 ZIP Criado com Sucesso!") + except Exception as e: + mprint(f"🚨 ERRO ao comprimir ZIP: {e}") + + # Movemos para a tela de CLOUD e batemos a foto Thumbnail baseada no nome f_name + import vr4life_cloud as cld + cld.finalize_export(ui, p_glb, f_name) + else: mprint("🚨 ERRO: Falha na geracao. Vefique o log.") diff --git a/vr4life_ui.py b/vr4life_ui.py index 41a5045..308f178 100644 --- a/vr4life_ui.py +++ b/vr4life_ui.py @@ -22,7 +22,7 @@ class AutoBakeManager(QtWidgets.QDialog): def __init__(self): super(AutoBakeManager, self).__init__(get_max_window_safe()) self.setWindowTitle("VR4LIFE AUTO-BAKE V313 - MODULAR ENTERPRISE") - self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowMinimizeButtonHint | QtCore.Qt.WindowMaximizeButtonHint | QtCore.Qt.WindowCloseButtonHint) + self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowMinimizeButtonHint | QtCore.Qt.WindowMaximizeButtonHint | QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowStaysOnTopHint) self.resize(570, 890); self.bake_items = []; self._is_cancelled = False p = self.palette(); p.setColor(QtGui.QPalette.Window, QtGui.QColor(43, 43, 43)); self.setPalette(p); self.setAutoFillBackground(True) self.init_ui() @@ -40,7 +40,7 @@ class AutoBakeManager(QtWidgets.QDialog): # ABA 1 t_gen = QtWidgets.QWidget(); l_gen = QtWidgets.QVBoxLayout(t_gen); f_gen = QtWidgets.QFormLayout() - self.btn_ref = QtWidgets.QPushButton("🔄 RECARREGAR MAPAS"); self.btn_ref.setStyleSheet("background: #555; color: white; height: 35px;"); self.btn_ref.clicked.connect(lambda: eng.load_bake_elements(self)) + self.btn_ref = QtWidgets.QPushButton("🔄 RECARREGAR MAPAS"); self.btn_ref.setStyleSheet("background: #444; color: white; border: 1px solid #222; font-weight: bold; height: 35px;"); self.btn_ref.clicked.connect(lambda: eng.load_bake_elements(self)) self.cmb_bake_elem = QtWidgets.QComboBox(); self.cmb_bake_elem.setStyleSheet("background: white; color: black; font-weight: bold;") self.chk_denoise = QtWidgets.QCheckBox("✨ Ativar Denoiser IA"); self.chk_denoise.setStyleSheet("color: #00FFFF; font-weight: bold;"); self.chk_denoise.setChecked(True) self.spn_light_boost = QtWidgets.QSpinBox(); self.spn_light_boost.setStyleSheet("background: white; color: black; font-weight: bold;"); self.spn_light_boost.setRange(0, 50); self.spn_light_boost.setSuffix("%"); self.spn_light_boost.setToolTip("Aumenta temporariamente a intensidade de todas as luzes da cena para o Bake.") @@ -89,18 +89,57 @@ class AutoBakeManager(QtWidgets.QDialog): l_tex.addLayout(f_tex); l_tex.addStretch(); self.tabs.addTab(t_tex, "🎨 3. Textura") t_cld = QtWidgets.QWidget(); l_cld = QtWidgets.QVBoxLayout(t_cld); f_cld = QtWidgets.QFormLayout() + + self.config_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "vr4_config.json") + saved_hash = "" + if os.path.exists(self.config_path): + try: + with open(self.config_path, "r", encoding="utf-8") as f: saved_hash = json.load(f).get("api_hash", "") + except: pass + self.e_hash = QtWidgets.QLineEdit(); self.e_hash.setStyleSheet("background: white; color: black; letter-spacing: 2px;"); self.e_hash.setEchoMode(QtWidgets.QLineEdit.Password) - self.b_conn = QtWidgets.QPushButton("🔄 Conectar CMS"); self.b_conn.setStyleSheet("background: #2E8B57; color: white; height: 30px;"); self.b_conn.clicked.connect(lambda: cld.mock_connect_api(self)) + self.e_hash.setText(saved_hash) + + def save_hash_config(): + try: + with open(self.config_path, "w", encoding="utf-8") as f: json.dump({"api_hash": self.e_hash.text()}, f) + except: pass + self.e_hash.textChanged.connect(save_hash_config) + + self.b_conn = QtWidgets.QPushButton("🔄 Conectar CMS"); self.b_conn.setStyleSheet("background: #2E8B57; color: white; font-weight: bold; border: 1px solid #1c5234; height: 30px;"); self.b_conn.clicked.connect(lambda: cld.mock_connect_api(self)) self.c_chn = QtWidgets.QComboBox(); self.c_chn.setStyleSheet("background: #CCC; color: black;"); self.c_chn.setEnabled(False) - self.c_pst = QtWidgets.QComboBox(); self.c_pst.setStyleSheet("background: #CCC; color: black;"); self.c_pst.setEnabled(False); self.c_pst.currentIndexChanged.connect(lambda idx: cld.on_post_changed(self, idx)) + def ui_do_fetch(idx=None): cld.fetch_posts_for_channel(self) + self.c_chn.currentIndexChanged.connect(ui_do_fetch) + + self.rdo_add = QtWidgets.QRadioButton("Novo Projeto"); self.rdo_add.setStyleSheet("color: white; font-weight: bold;"); self.rdo_add.setChecked(True); self.rdo_add.setEnabled(False) + self.rdo_edt = QtWidgets.QRadioButton("Atualizar Antigo"); self.rdo_edt.setStyleSheet("color: white; font-weight: bold;"); self.rdo_edt.setEnabled(False) + + def ui_do_post_change(*args): cld.on_post_changed(self) + def ui_do_fetch_edt(checked): + if checked: cld.fetch_posts_for_channel(self) + + self.rdo_add.toggled.connect(ui_do_post_change) + self.rdo_edt.toggled.connect(ui_do_post_change) + self.rdo_edt.toggled.connect(ui_do_fetch_edt) + h_act = QtWidgets.QHBoxLayout(); h_act.addWidget(self.rdo_add); h_act.addWidget(self.rdo_edt) + + self.c_pst = QtWidgets.QComboBox(); self.c_pst.setStyleSheet("background: #CCC; color: black;"); self.c_pst.setEnabled(False) + self.c_pst.currentIndexChanged.connect(ui_do_post_change) + self.e_tit = QtWidgets.QLineEdit(); self.e_tit.setStyleSheet("background: #CCC; color: black;"); self.e_tit.setEnabled(False) self.e_mp3 = QtWidgets.QLineEdit(); self.e_mp3.setStyleSheet("background: #CCC; color: black;"); self.e_mp3.setEnabled(False) self.b_mp3 = QtWidgets.QPushButton("🎵 MP3"); self.b_mp3.setStyleSheet("background: #555; color: white;"); self.b_mp3.setEnabled(False); self.b_mp3.clicked.connect(self.browse_mp3) h_mp3 = QtWidgets.QHBoxLayout(); h_mp3.addWidget(self.e_mp3); h_mp3.addWidget(self.b_mp3) - self.chk_tmb = QtWidgets.QCheckBox("📸 Thumbnail"); self.chk_tmb.setStyleSheet("color: gray; font-weight: bold;"); self.chk_tmb.setEnabled(False) - self.chk_up = QtWidgets.QCheckBox("🚀 Upload Multipart Automático"); self.chk_up.setStyleSheet("color: gray; font-weight: bold;"); self.chk_up.setEnabled(False) - f_cld.addRow("Chave:", self.e_hash); f_cld.addRow("", self.b_conn); f_cld.addRow("Canal:", self.c_chn); f_cld.addRow("Ação:", self.c_pst); f_cld.addRow("Título:", self.e_tit); f_cld.addRow("Som:", h_mp3) - f_cld.addRow("", self.chk_tmb); f_cld.addRow("", self.chk_up) + self.b_tmb = QtWidgets.QPushButton("📸 Gerar Thumbnail"); self.b_tmb.setStyleSheet("background: #FF8C00; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b_tmb.setEnabled(False) + self.b_tmb.clicked.connect(lambda: cld.generate_thumbnail(self)) + self.b_up = QtWidgets.QPushButton("☁️ Enviar para Nuvem"); self.b_up.setStyleSheet("background: #0078D7; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b_up.setEnabled(False) + self.b_up.clicked.connect(lambda: cld.upload_to_cloud(self)) + h_bot = QtWidgets.QHBoxLayout(); h_bot.addWidget(self.b_tmb); h_bot.addWidget(self.b_up) + + f_cld.addRow("Chave:", self.e_hash); f_cld.addRow("", self.b_conn); f_cld.addRow("Canal:", self.c_chn) + f_cld.addRow("Ação:", h_act); f_cld.addRow("Projetos:", self.c_pst) + f_cld.addRow("Título:", self.e_tit); f_cld.addRow("Som:", h_mp3) + f_cld.addRow("", h_bot) l_cld.addLayout(f_cld); l_cld.addStretch(); self.tabs.addTab(t_cld, "☁️ 4. API Cloud") layout.addWidget(self.tabs) @@ -148,10 +187,9 @@ class AutoBakeManager(QtWidgets.QDialog): self.b5 = QtWidgets.QPushButton("P5: UV"); self.b5.setStyleSheet("background: #E0E0E0; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b5.clicked.connect(lambda: eng.prepare_mesh_v19(self)) self.b6 = QtWidgets.QPushButton("P6: Bake"); self.b6.setStyleSheet("background: #E0E0E0; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b6.clicked.connect(lambda: eng.process_bake_logic_v19(self, False)) self.b7 = QtWidgets.QPushButton("P7: Exportar"); self.b7.setStyleSheet("background: #FFD700; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b7.clicked.connect(lambda: eng.export_glb_v19(self)) - self.b8 = QtWidgets.QPushButton("P8: Inspec."); self.b8.setStyleSheet("background: #9370DB; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b8.clicked.connect(lambda: eng.inspect_uv(self)) self.b_clear = QtWidgets.QPushButton("🗑️ Limpar"); self.b_clear.setStyleSheet("background: #666; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b_clear.clicked.connect(self.clear_list) - for b in [self.b1, self.b3, self.b5, self.b6, self.b7, self.b8, self.b_clear]: h_l.addWidget(b) + for b in [self.b_clear, self.b1, self.b3, self.b5, self.b6, self.b7]: h_l.addWidget(b) layout.addLayout(h_l) self.btn_cancel = QtWidgets.QPushButton("CANCELAR OPERAÇÃO"); self.btn_cancel.setStyleSheet("background: #FF0000; color: white; font-weight: bold; height: 40px; border-radius: 8px; margin-top: 10px;"); self.btn_cancel.clicked.connect(self.cancel_all)