diff --git a/instalador/Instalador_Online_VR4Life.py b/instalador/Instalador_Online_VR4Life.py new file mode 100644 index 0000000..c981f12 --- /dev/null +++ b/instalador/Instalador_Online_VR4Life.py @@ -0,0 +1,58 @@ +import os +import urllib.request +from pymxs import runtime as rt + +# ========================================== +# CONFIGURAÇÕES DO GITEA - IMMERSE GAMES +# ========================================== +GITEA_RAW_URL = "https://git.immersegame.com/immersegame/vr4life-3dmax-plugin/raw/branch/main/" +GITEA_TOKEN = "efebcde14ce96a2b80d0b3f207991bc155018ab8" + +# Descobre a pasta segura de Scripts do Usuário do próprio 3ds Max +user_scripts_dir = rt.getDir(rt.name("userScripts")) +PLUGIN_DIR = os.path.join(user_scripts_dir, "VR4Life_Plugin").replace("\\", "/") + +FILES_TO_DOWNLOAD = [ + "vr4life_ui.py", + "vr4life_engine.py", + "vr4life_cloud.py", + "run_vr4life.py", + "vr4life_updater.py", + "install_vr4life.py", + "version.txt" +] + +def install_from_cloud(): + rt.clearListener() + print("=== INICIANDO INSTALAÇÃO ONLINE VR4LIFE ===") + + if not os.path.exists(PLUGIN_DIR): + os.makedirs(PLUGIN_DIR) + + try: + for file_name in FILES_TO_DOWNLOAD: + remote_url = GITEA_RAW_URL + file_name + local_path = os.path.join(PLUGIN_DIR, file_name).replace("\\", "/") + + print(f"Baixando: {file_name}...") + req = urllib.request.Request(remote_url) + req.add_header("Authorization", f"token {GITEA_TOKEN}") + + response = urllib.request.urlopen(req) + remote_code = response.read().decode('utf-8') + + with open(local_path, "w", encoding="utf-8") as f: + f.write(remote_code) + + print("Download concluído! Configurando menu do 3ds Max...") + + install_script = os.path.join(PLUGIN_DIR, "install_vr4life.py").replace("\\", "/") + rt.python.ExecuteFile(install_script) + + except Exception as e: + msg = f"Erro ao baixar os arquivos do Gitea.\nVerifique a internet, URL ou o Token de acesso.\n\nDetalhe técnico: {str(e)}" + rt.messageBox(msg, title="Erro de Instalação") + print(msg) + +if __name__ == "__main__": + install_from_cloud() \ No newline at end of file diff --git a/instalador/VR4Life_Installer.mzp b/instalador/VR4Life_Installer.mzp new file mode 100644 index 0000000..f0f79ea Binary files /dev/null and b/instalador/VR4Life_Installer.mzp differ diff --git a/instalador/mzp.run b/instalador/mzp.run new file mode 100644 index 0000000..6f45b55 --- /dev/null +++ b/instalador/mzp.run @@ -0,0 +1,8 @@ +name "VR4Life Web Installer" +version "1.0" + +extract to $temp\VR4Life_WebInstaller +copy *.* to $temp\VR4Life_WebInstaller + +run "run_installer.ms" +drop "run_installer.ms" \ No newline at end of file diff --git a/instalador/run_installer.ms b/instalador/run_installer.ms new file mode 100644 index 0000000..b1ce35a --- /dev/null +++ b/instalador/run_installer.ms @@ -0,0 +1,7 @@ +-- O MZP vai extrair os arquivos para a pasta Temporária do Max. +local py_script = (getDir #temp) + "\\VR4Life_WebInstaller\\Instalador_Online_VR4Life.py" +if doesFileExist py_script then ( + python.ExecuteFile py_script +) else ( + messageBox "Erro: Arquivo Python não encontrado no pacote MZP." title:"Erro" +) \ No newline at end of file diff --git a/install_vr4life.py b/install_vr4life.py new file mode 100644 index 0000000..138f505 --- /dev/null +++ b/install_vr4life.py @@ -0,0 +1,49 @@ +import os +from pymxs import runtime as rt + +def install_plugin(): + # 1. Descobre a pasta exata onde este instalador foi salvo pelo usuário + script_dir = os.path.dirname(os.path.realpath(__file__)) + py_file = os.path.join(script_dir, "run_vr4life.py").replace("\\", "/") + + # 2. Cria o MacroScript (A Ponte) dinamicamente injetando código no Max + macro_code = f""" + macroScript VR4Life_AutoBake category:"VR4Life" buttonText:"Painel Auto-Bake" tooltip:"Abre o motor de otimização VR4Life / Zombisco" + ( + python.ExecuteFile @"{py_file}" + ) + """ + rt.execute(macro_code) + + # 3. Acessa o Gerenciador de Menus do 3ds Max + menu_name = "VR4Life" + main_menu_bar = rt.menuMan.getMainMenuBar() + + # Verifica se o menu já existe (para evitar duplicações se o usuário instalar 2x) + existing_menu = rt.menuMan.findMenu(menu_name) + if existing_menu: + rt.menuMan.unRegisterMenu(existing_menu) + + # Cria o menu Dropdown principal + new_menu = rt.menuMan.createMenu(menu_name) + + # Cria o item clicável que aponta para o MacroScript (A Ponte) + menu_item = rt.menuMan.createActionItem("VR4Life_AutoBake", "VR4Life") + new_menu.addItem(menu_item, -1) + + # Injeta o menu lá no topo, na barra principal do 3ds Max + sub_menu_item = rt.menuMan.createSubMenuItem(menu_name, new_menu) + + # Adiciona no final da barra (usando o index atual) + index = main_menu_bar.numItems() + main_menu_bar.addItem(sub_menu_item, index) + + # Atualiza a interface gráfica do 3ds Max na mesma hora + rt.menuMan.updateMenuBar() + + # Mensagem de Sucesso + msg = "Plugin VR4Life instalado com sucesso!\n\nOlhe para a barra superior do seu 3ds Max, o menu já está lá pronto para uso." + rt.messageBox(msg, title="Instalação Concluída") + +if __name__ == "__main__": + install_plugin() \ No newline at end of file diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..e69de29 diff --git a/vr4life_engine.py b/vr4life_engine.py index cd72783..97220b1 100644 --- a/vr4life_engine.py +++ b/vr4life_engine.py @@ -49,21 +49,39 @@ def load_selection(ui): ui.tree.addTopLevelItem(i); ui.bake_items.append({'name': d['name'], 'item': i}) ui.pb.setFormat(f"Carregados: {len(tl)}"); ui.pb.setValue(0); ui.upd_res_col() + def attach_grouped_objects(ui, from_auto=False): - ui.pb.setFormat("Fundindo grupos (Atlas)..."); QtWidgets.QApplication.processEvents() - ms = """( + thr = ui.spn_max_sz.value() + ui.pb.setFormat("Filtando e Fundindo grupos..."); QtWidgets.QApplication.processEvents() + ms = f"""( local act = 0; local new_objs = #(); local orig_sel = selection as array local loose = for o in orig_sel where not isGroupHead o and not isGroupMember o collect o local heads = for o in orig_sel where isGroupHead o collect o + local limit = {thr} + for h in heads do ( local cg = for c in h.children where superclassof c == GeometryClass and classof c != TargetObject collect c - if cg.count > 0 do ( - local b = cg[1]; if not isKindOf b Editable_Poly do try(convertToPoly b)catch() + local valid_cg = #() + + -- O SEGURANÇA DA BALADA: Expulsa quem for maior que o limite + for c in cg do ( + local md = amax #(abs(c.max.x - c.min.x), abs(c.max.y - c.min.y), abs(c.max.z - c.min.z)) + if md > limit then ( + setGroupMember c false + append loose c -- Joga pros VIPs soltos + ) else ( + append valid_cg c -- Fica pra ser fundido + ) + ) + + if valid_cg.count > 0 do ( + local b = valid_cg[1]; if not isKindOf b Editable_Poly do try(convertToPoly b)catch() if isKindOf b Editable_Poly do ( - for i = 2 to cg.count do ( local n = cg[i]; if not isKindOf n Editable_Poly do try(convertToPoly n)catch(); if isKindOf n Editable_Poly do try(polyOp.attach b n)catch() ) - b.name = h.name; setGroupMember b false; append new_objs b; delete h; act += 1 + for i = 2 to valid_cg.count do ( local n = valid_cg[i]; if not isKindOf n Editable_Poly do try(convertToPoly n)catch(); if isKindOf n Editable_Poly do try(polyOp.attach b n)catch() ) + b.name = h.name; setGroupMember b false; append new_objs b; act += 1 ) ) + try(delete h)catch() ) local final_sel = loose; for no in new_objs do append final_sel no; if final_sel.count > 0 do select final_sel; act )""" @@ -71,8 +89,8 @@ def attach_grouped_objects(ui, from_auto=False): act = rt.execute(ms) load_selection(ui) if not from_auto: - if act > 0: QtWidgets.QMessageBox.information(ui, "Atlas Welder", f"Sucesso! {act} Grupos fundidos.\nObjetos soltos preservados.") - else: QtWidgets.QMessageBox.information(ui, "Aviso", "Nenhum Grupo encontrado.") + if act > 0: QtWidgets.QMessageBox.information(ui, "Atlas Welder", f"Sucesso! {act} Grupos processados.\nObjetos gigantes foram ejetados do grupo com segurança!") + else: QtWidgets.QMessageBox.information(ui, "Aviso", "Nenhum Grupo encontrado na seleção.") except Exception as e: print("Erro Solda:", e) ui.pb.setValue(0); ui.pb.setFormat("Pronto") @@ -203,7 +221,7 @@ def process_bake_logic(ui, auto_export=False): render rendertype:#bakeSelected vfb:true quiet:true outputfile:"{4}" )""".format(d['name'], el, cr, ext, t_rnd, str(i_cor).lower(), str(u_den).lower()) rt.execute(ms) - + wt = 0 while not os.path.exists(t_rnd) and wt < 60: time.sleep(0.5); wt += 0.5; QtWidgets.QApplication.processEvents() @@ -228,4 +246,19 @@ def process_bake_logic(ui, auto_export=False): else: d['item'].setText(1, "Erro Save") except: d['item'].setText(1, "Erro Render") ui.pb.setValue(tot); ui.pb.setFormat("Bake Concluído!") - if auto_export and not ui._is_cancelled: cld.finalize_export(ui, p_bk, ui.edt_p_glb.text()) \ No newline at end of file + if auto_export and not ui._is_cancelled: cld.finalize_export(ui, p_bk, ui.edt_p_glb.text()) + +def upd_res_col(ui): + a_r = ui.chk_a_res.isChecked(); m_r = ui.spn_res.value() + for d in ui.bake_items: + o = rt.getNodeByName(d['name']) + if o: + try: + md = max(abs(o.max.x - o.min.x), abs(o.max.y - o.min.y), abs(o.max.z - o.min.z)) + r = 256 if md <= ui.s256.value() else 512 if md <= ui.s512.value() else 1024 if md <= ui.s1024.value() else m_r if a_r else m_r + d['item'].setText(3, f"{r}px"); d['item'].setText(4, f"{md:.1f}") + if r == 256: d['item'].setForeground(3, QtGui.QColor(0, 255, 255)) + elif r == 512: d['item'].setForeground(3, QtGui.QColor(0, 255, 0)) + elif r == 1024: d['item'].setForeground(3, QtGui.QColor(255, 165, 0)) + else: d['item'].setForeground(3, QtGui.QColor(255, 50, 50)) + except: pass \ No newline at end of file diff --git a/vr4life_updater.py b/vr4life_updater.py new file mode 100644 index 0000000..7b5991a --- /dev/null +++ b/vr4life_updater.py @@ -0,0 +1,92 @@ +import os +import urllib.request +from pymxs import runtime as rt +try: from PySide6 import QtWidgets +except ImportError: from PySide2 import QtWidgets + +# URL Oficial do repositório da Immerse Games (Lendo a branch 'main') +# Obs: Se o seu Gitea estiver usando 'master' em vez de 'main', basta trocar a última palavra. +GITEA_RAW_URL = "https://git.immersegame.com/immersegame/vr4life-3dmax-plugin/raw/branch/main/" + +# Token de Leitura do Gitea +GITEA_TOKEN = "efebcde14ce96a2b80d0b3f207991bc155018ab8" + +FILES_TO_UPDATE = [ + "vr4life_ui.py", + "vr4life_engine.py", + "vr4life_cloud.py", + "run_vr4life.py", + "vr4life_updater.py" +] + +def check_and_update(ui_parent=None): + script_dir = os.path.dirname(os.path.realpath(__file__)) + local_version_file = os.path.join(script_dir, "version.txt").replace("\\", "/") + + if ui_parent: + ui_parent.pb.setFormat("Autenticando e checando versão no Gitea..."); QtWidgets.QApplication.processEvents() + + try: + # 1. Lê a versão local (Se não existir, assume 0.0) + local_version = "0.0" + if os.path.exists(local_version_file): + with open(local_version_file, "r") as f: + local_version = f.read().strip() + + # 2. Bate na Nuvem com o Token para ler o version.txt + remote_version_url = GITEA_RAW_URL + "version.txt" + req_version = urllib.request.Request(remote_version_url) + req_version.add_header("Authorization", f"token {GITEA_TOKEN}") + + response_version = urllib.request.urlopen(req_version) + remote_version = response_version.read().decode('utf-8').strip() + + # 3. Compara as versões: Só baixa se a do Gitea for mais nova + if remote_version == local_version: + if ui_parent: + ui_parent.pb.setFormat("Sistema já está atualizado!"); ui_parent.pb.setValue(100) + QtWidgets.QMessageBox.information(ui_parent, "Atualizador", f"Você já está usando a versão mais recente ({local_version}).") + return + + # 4. Inicia o Download Seguro dos Arquivos + if ui_parent: + ui_parent.pb.setFormat(f"Nova versão {remote_version} encontrada! Baixando..."); QtWidgets.QApplication.processEvents() + + updated_count = 0 + for file_name in FILES_TO_UPDATE: + remote_url = GITEA_RAW_URL + file_name + local_path = os.path.join(script_dir, file_name).replace("\\", "/") + + if ui_parent: + ui_parent.pb.setFormat(f"Baixando {file_name}..."); QtWidgets.QApplication.processEvents() + + # Puxa o código com o Token + req = urllib.request.Request(remote_url) + req.add_header("Authorization", f"token {GITEA_TOKEN}") + response = urllib.request.urlopen(req) + remote_code = response.read().decode('utf-8') + + # Sobrescreve na máquina local + with open(local_path, "w", encoding="utf-8") as f: + f.write(remote_code) + + updated_count += 1 + + # 5. Salva a nova versão local para a próxima checagem + with open(local_version_file, "w") as f: + f.write(remote_version) + + msg = f"Sucesso! Plugin atualizado para a versão {remote_version} ({updated_count} arquivos).\n\nPor favor, feche esta janela e abra o plugin novamente pelo menu superior." + if ui_parent: + ui_parent.pb.setFormat("Atualização Concluída!"); ui_parent.pb.setValue(100) + QtWidgets.QMessageBox.information(ui_parent, "Update VR4Life", msg) + else: + rt.messageBox(msg, title="Update VR4Life") + + except Exception as e: + erro_msg = f"Falha de Autenticação ou Download.\nVerifique seu Token e URL do Gitea.\n\nDetalhe técnico: {str(e)}" + if ui_parent: + QtWidgets.QMessageBox.critical(ui_parent, "Erro de Update", erro_msg) + ui_parent.pb.setFormat("Pronto"); ui_parent.pb.setValue(0) + else: + rt.messageBox(erro_msg, title="Erro de Update") \ No newline at end of file