commit 26679379883457262d955a0eee686e08ff3b8096 Author: henrique Date: Sun Feb 22 18:03:47 2026 -0300 primeira versao diff --git a/run_vr4life.py b/run_vr4life.py new file mode 100644 index 0000000..6101b85 --- /dev/null +++ b/run_vr4life.py @@ -0,0 +1,15 @@ +import sys, os +import importlib + +# Garante que o 3ds Max enxergue a sua pasta de plugins +script_dir = os.path.dirname(os.path.realpath(__file__)) +if script_dir not in sys.path: + sys.path.append(script_dir) + +# Importa a sua nova UI limpa e recarrega para sempre pegar atualizações +import vr4life_ui +importlib.reload(vr4life_ui) + +if __name__ == "__main__": + app = vr4life_ui.AutoBakeManager() + app.show() \ No newline at end of file diff --git a/vr4life_cloud.py b/vr4life_cloud.py new file mode 100644 index 0000000..66eb7f0 --- /dev/null +++ b/vr4life_cloud.py @@ -0,0 +1,70 @@ +import os, time, zipfile +from pymxs import runtime as rt +try: from PySide6 import QtWidgets +except ImportError: from PySide2 import QtWidgets + +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) + +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;") + +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() + for d in tgs: + if ui._is_cancelled: break + if "Bake" in d['item'].text(1) or d['item'].text(1) in ["Já existe", "Mat OK"]: + ip = os.path.join(p_bk, f"{d['name']}_B.jpg").replace("\\", "/") + if os.path.exists(ip): + try: + 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 = 0.8; m.metalness = 0.0; o.material = m ) )""") + fo.append(rt.getNodeByName(d['name'])); d['item'].setText(1, "Mat OK") + except: d['item'].setText(1, "Erro Mat") + + 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) + + 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 + + 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 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) + ui.pb.setFormat("Pronto"); ui.pb.setValue(0) \ No newline at end of file diff --git a/vr4life_engine.py b/vr4life_engine.py new file mode 100644 index 0000000..cd72783 --- /dev/null +++ b/vr4life_engine.py @@ -0,0 +1,231 @@ +import os, time, subprocess +from pymxs import runtime as rt +try: from PySide6 import QtWidgets, QtCore, QtGui +except ImportError: from PySide2 import QtWidgets, QtCore, QtGui +import vr4life_cloud as cld + +def get_vdenoise_path(): + for k, v in os.environ.items(): + if "VRAY" in k.upper() and "MAX" in k.upper() and "MAIN" in k.upper(): + p = os.path.join(v, "vdenoise.exe") + if os.path.exists(p): return f'"{p}"' + return "vdenoise.exe" + +def deep_log_and_kill(): + try: + time.sleep(0.5); tgts = ["3d66", "V-Ray", "Buffer", "RGBA", "Render"] + for w in rt.windows.getChildrenHWND(0): + for t in tgts: + if t.lower() in str(w[4]).lower(): rt.windows.sendMessage(w[0], 0x0010, 0, 0); rt.windows.sendMessage(w[0], 0x0002, 0, 0) + except: pass + +def load_bake_elements(ui): + ui.cmb_bake_elem.clear() + found = ["CompleteMap", "VRayCompleteMap", "Corona_Beauty", "CShading_Beauty"] + try: + r_list = rt.execute("for c in bake_elements.classes collect (c as string)") + if r_list: + for c in r_list: + if str(c) not in found: found.append(str(c)) + except: pass + ui.cmb_bake_elem.addItems(sorted(list(set(found)))) + try: + rnd = str(rt.execute("renderers.current as string")) + t = "VRayCompleteMap" if "V_Ray" in rnd else "Corona_Beauty" if "Corona" in rnd else "CompleteMap" + idx = ui.cmb_bake_elem.findText(t) + if idx >= 0: ui.cmb_bake_elem.setCurrentIndex(idx) + except: pass + +def load_selection(ui): + ui.tree.clear(); ui.bake_items = [] + ms = """( local g = #(); fn ex objs = ( for o in objs do ( if isGroupHead o then ( setGroupOpen o true; ex o.children ) else ( if superclassof o == GeometryClass and (classOf o != TargetObject) and (canConvertTo o Editable_Poly) do appendIfUnique g o ) ) ); ex selection; g )""" + sel = rt.execute(ms) + if not sel: return + tl = [{'name': str(o.name), 'poly': rt.getPolygonCount(o)[0] if rt.canConvertTo(o, rt.Editable_Poly) else 0} for o in sel] + tl.sort(key=lambda x: x['poly'], reverse=True) + for d in tl: + i = QtWidgets.QTreeWidgetItem([d['name'], "Pronto", f"{d['poly']:,}", "", ""]) + i.setFlags(i.flags() | QtCore.Qt.ItemIsUserCheckable); i.setCheckState(0, QtCore.Qt.Checked) + 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 = """( + 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 + 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() + 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 + ) + ) + ) + local final_sel = loose; for no in new_objs do append final_sel no; if final_sel.count > 0 do select final_sel; act + )""" + try: + 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.") + except Exception as e: print("Erro Solda:", e) + ui.pb.setValue(0); ui.pb.setFormat("Pronto") + +def optimize_geometry(ui): + sp = ui.spn_pct.value(); tg = ui.spn_min_poly.value(); tgs = ui.get_processable_items(); tot = len(tgs) + ui.pb.setMaximum(tot); ui.pb.setValue(0); rt.execute("max modify mode") + for i, d in enumerate(tgs): + if ui._is_cancelled: break + ui.pb.setFormat(f"Opt ({i+1}/{tot}): {d['name']}..."); ui.pb.setValue(i); QtWidgets.QApplication.processEvents() + o = rt.getNodeByName(d['name']) + if o: + try: + cp = rt.getPolygonCount(o)[0] if rt.canConvertTo(o, rt.Editable_Poly) else 0 + if cp <= tg: d['item'].setText(1, "Já < Meta") + elif cp < 50: d['item'].setText(1, "Geo Base") + else: + rt.select(o); p = 0 + while cp > tg and cp >= 50 and p < 20: + if not rt.isKindOf(o, rt.Editable_Poly): rt.convertTo(o, rt.Editable_Poly) + opt = rt.ProOptimizer(); rt.addModifier(o, opt); opt.KeepTextures = False; opt.KeepNormals = False; opt.Calculate = True; opt.VertexPercent = sp + rt.collapseStack(o) + if not rt.isKindOf(o, rt.Editable_Poly): rt.convertTo(o, rt.Editable_Poly) + np = rt.getPolygonCount(o)[0] + if np >= cp or np == 0: break + cp = np; p += 1; QtWidgets.QApplication.processEvents() + d['item'].setText(2, f"{cp:,}"); d['item'].setText(1, f"Opt ({p}x)") + except: d['item'].setText(1, "Erro Opt") + ui.pb.setValue(tot); ui.pb.setFormat("Opt Concluída!") + +def slice_large_objects(ui, from_auto=False): + if not ui.bake_items: return + thr = ui.spn_max_sz.value(); act = 0; rt.execute("max modify mode"); tgs = ui.get_processable_items() + fn = []; pn = [d['name'] for d in tgs] + for d in ui.bake_items: + if d['name'] not in pn: fn.append(d['name']) + tot = len(tgs); ui.pb.setMaximum(tot); ui.pb.setValue(0) + for i, d in enumerate(tgs): + if ui._is_cancelled: break + ui.pb.setFormat(f"Cortando ({i+1}/{tot}): {d['name']}..."); ui.pb.setValue(i); QtWidgets.QApplication.processEvents() + chk = [d['name']]; sc = 0 + while len(chk) > 0 and sc < 300: + QtWidgets.QApplication.processEvents() + if ui._is_cancelled: break + cn = chk.pop(0); o = rt.getNodeByName(cn) + if not o: continue + md = max(abs(o.max.x - o.min.x), abs(o.max.y - o.min.y), abs(o.max.z - o.min.z)) + if md > thr: + ax = "X" if md == abs(o.max.x - o.min.x) else "Y" if md == abs(o.max.y - o.min.y) else "Z" + ms = """( + fn bsp objN ax = ( + local o = getNodeByName objN; if o == undefined or not (canConvertTo o Editable_Poly) do return #() + if not isKindOf o Editable_Poly do try(convertToPoly o)catch(); if not isKindOf o Editable_Poly do return #() + local v = polyop.getNumVerts o; if v > 0 do ( try(o.weldThreshold = 0.001)catch(); try(polyop.weldVertsByThreshold o #{{1..v}})catch() ) + CenterPivot o; local o2 = copy o name:(uniqueName (o.name + "_F")) + local rot = if ax == "X" then eulerAngles 0 90 0 else if ax == "Y" then eulerAngles 90 0 0 else eulerAngles 0 0 0 + local s1 = SliceModifier(); s1.Slice_Type = 2; s1.slice_plane.rotation = rot; addModifier o s1; try(addModifier o (Cap_Holes()))catch(); collapseStack o + local s2 = SliceModifier(); s2.Slice_Type = 3; s2.slice_plane.rotation = rot; addModifier o2 s2; try(addModifier o2 (Cap_Holes()))catch(); collapseStack o2 + if not isKindOf o Editable_Poly do try(convertToPoly o)catch(); if not isKindOf o2 Editable_Poly do try(convertToPoly o2)catch() + local f1 = if isValidNode o then polyop.getNumFaces o else 0; local f2 = if isValidNode o2 then polyop.getNumFaces o2 else 0 + local r = #(); if f1 < 5 then try(delete o)catch() else append r o.name; if f2 < 5 then try(delete o2)catch() else append r o2.name; return r + ); bsp "{0}" "{1}" + )""".format(cn, ax) + try: + n_p = rt.execute(ms) + if n_p and len(n_p) > 1: + act += 1 + for p in n_p: + po = rt.getNodeByName(p) + if po: + pd = max(abs(po.max.x - po.min.x), abs(po.max.y - po.min.y), abs(po.max.z - po.min.z)) + if pd >= md * 0.98: fn.append(p) + else: chk.append(p) + else: + for p in n_p: fn.append(p) if n_p else fn.append(cn) + except: fn.append(cn) + else: fn.append(cn) + sc += 1 + ui.pb.setValue(tot); ui.pb.setFormat("Fatiador Concluído!") + rt.clearSelection(); sel = [rt.getNodeByName(n) for n in fn if rt.getNodeByName(n)]; rt.select(sel) + if act > 0: load_selection(ui) + +def prepare_mesh(ui): + tgs = ui.get_processable_items(); tot = len(tgs); ui.pb.setMaximum(tot); ui.pb.setValue(0); rt.execute("max modify mode") + for i, d in enumerate(tgs): + if ui._is_cancelled: break + ui.pb.setFormat(f"UV ({i+1}/{tot}): {d['name']}..."); ui.pb.setValue(i); QtWidgets.QApplication.processEvents() + o = rt.getNodeByName(d['name']) + if o: + rt.select(o) + try: + if not rt.isKindOf(o, rt.Editable_Poly): rt.convertTo(o, rt.Editable_Poly) + rt.execute(f"""( local o = getNodeByName "{d['name']}"; local m = Unwrap_UVW(); addModifier o m; m.setMapChannel 1; m.setTVSubObjectMode 3; local nf = polyOp.getNumFaces o; m.selectFaces #{{1..nf}}; m.flattenMap 45.0 #() 0.002 true 0 true false; m.pack 1 0.002 true false false; collapseStack o )""") + if not rt.isKindOf(o, rt.Editable_Poly): rt.convertTo(o, rt.Editable_Poly) + d['item'].setText(1, "UV Packed") + except: d['item'].setText(1, "Erro UV") + ui.pb.setValue(tot); ui.pb.setFormat("UV Pack Concluído!") + +def process_bake_logic(ui, auto_export=False): + el = ui.cmb_bake_elem.currentText(); p_bk = ui.edt_p_bake.text() + rnd = str(rt.execute("renderers.current as string")); i_vr = "V_Ray" in rnd; i_cor = "Corona" in rnd + u_den = ui.chk_denoise.isChecked(); a_res = ui.chk_a_res.isChecked() + if not os.path.exists(p_bk): os.makedirs(p_bk) + tgs = ui.get_processable_items(); tot = len(tgs); ui.pb.setMaximum(tot); ui.pb.setValue(0) + + for i, d in enumerate(tgs): + if ui._is_cancelled: break + ui.pb.setFormat(f"Bake ({i+1}/{tot}): {d['name']}..."); ui.pb.setValue(i); QtWidgets.QApplication.processEvents() + t_jpg = os.path.join(p_bk, f"{d['name']}_B.jpg").replace("\\", "/") + if os.path.exists(t_jpg): d['item'].setText(1, "Já existe"); continue + + cr = ui.spn_res.value(); o = rt.getNodeByName(d['name']) + if o and a_res: + md = max(abs(o.max.x - o.min.x), abs(o.max.y - o.min.y), abs(o.max.z - o.min.z)) + cr = 256 if md <= ui.s256.value() else 512 if md <= ui.s512.value() else 1024 if md <= ui.s1024.value() else cr + + ext = ".exr" if (i_vr and u_den) else ".jpg" + t_rnd = t_jpg.replace(".jpg", ".exr") if ext == ".exr" else t_jpg + + rt.execute("max select none"); rt.select(o); rt.execute("max modify mode"); rt.execute("max zoomext sel all"); rt.redrawViews(); time.sleep(0.5) + + try: + rt.execute("freeSceneBitmaps(); try(vfbControl #clearimage)catch()") + ms = """( + local o = getNodeByName "{0}"; if {5} and {6} do try ( renderers.current.denoise_enable = true ) catch() + renderWidth = {2}; renderHeight = {2}; o.INodeBakeProperties.removeAllBakeElements() + local be = {1}(); be.outputSzX = {2}; be.outputSzY = {2}; be.fileType = "{3}"; be.fileName = "{4}" + o.INodeBakeProperties.addBakeElement be; o.INodeBakeProperties.bakeEnabled = true; o.INodeBakeProperties.bakeChannel = 1 + 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() + + if os.path.exists(t_rnd): + if i_vr and u_den: + d['item'].setText(1, "IA Limpando...") + c_in = t_rnd.replace("/", "\\"); c_out = t_rnd.replace(".exr", "_denoised.exr").replace("/", "\\") + try: + p = subprocess.Popen(f'{get_vdenoise_path()} -inputFile="{c_in}" -outputFile="{c_out}" -display=0', shell=True, creationflags=0x08000000); dt = 0 + while p.poll() is None and dt < 60: QtWidgets.QApplication.processEvents(); time.sleep(0.5); dt += 0.5 + if p.poll() is None: p.terminate() + except: pass + + f_exr = c_out.replace("\\", "/") if os.path.exists(c_out.replace("\\", "/")) else t_rnd + rt.execute(f"""( try(vfbControl #clearimage)catch(); try ( local bI = openBitmap @"{f_exr}"; if bI != undefined do ( local bO = bitmap bI.width bI.height filename:@"{t_jpg}" hdr:true; copy bI bO; save bO; close bI; close bO; free bI; free bO ) ) catch() )""") + rt.execute("freeSceneBitmaps(); gc light:true"); time.sleep(0.5) + try: os.remove(t_rnd); os.remove(c_out.replace("\\", "/")) + except: pass + + if os.path.exists(t_jpg): d['item'].setText(1, "Bake OK"); deep_log_and_kill() + else: d['item'].setText(1, "Erro Conv") + 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 diff --git a/vr4life_ui.py b/vr4life_ui.py new file mode 100644 index 0000000..61094c1 --- /dev/null +++ b/vr4life_ui.py @@ -0,0 +1,138 @@ +import os +from pymxs import runtime as rt +try: from PySide6 import QtWidgets, QtCore, QtGui +except ImportError: from PySide2 import QtWidgets, QtCore, QtGui + +import vr4life_engine as eng +import vr4life_cloud as cld +import importlib +importlib.reload(eng) +importlib.reload(cld) + +def get_max_window_safe(): + try: + if hasattr(rt, 'getMAXWindow'): return rt.getMAXWindow() + except: pass + for w in QtWidgets.QApplication.topLevelWidgets(): + if w.inherits("QMainWindow") or w.windowTitle().startswith("Autodesk 3ds Max"): return w + return None + +class AutoBakeManager(QtWidgets.QDialog): + def __init__(self): + super(AutoBakeManager, self).__init__(get_max_window_safe()) + self.setWindowTitle("VR4LIFE AUTO-BAKE V167.0 - MODULAR ENTERPRISE") + self.resize(950, 1380); 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() + rt.clearListener(); eng.load_bake_elements(self) + + def init_ui(self): + layout = QtWidgets.QVBoxLayout(self) + lbl_t = QtWidgets.QLabel("VR4LIFE - V167.0 (Modular)"); lbl_t.setAlignment(QtCore.Qt.AlignCenter); lbl_t.setStyleSheet("font-size: 24px; font-weight: bold; color: #00FF00;") + layout.addWidget(lbl_t) + + self.tabs = QtWidgets.QTabWidget() + self.tabs.setStyleSheet("QTabWidget::pane { border: 1px solid #777; background: #333; } QTabBar::tab { background: #444; color: #CCC; padding: 10px 15px; font-weight: bold; border: 1px solid #222; } QTabBar::tab:selected { background: #555; color: #00FF00; } QLabel { color: #FFF; font-weight: bold; }") + + # 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.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.edt_p_bake = QtWidgets.QLineEdit(rt.maxFilePath + "_BAKE_JPG\\" if rt.maxFilePath else "C:\\VR4_BAKE\\"); self.edt_p_bake.setStyleSheet("background: white; color: black;") + self.edt_p_glb = QtWidgets.QLineEdit(rt.maxFilePath + "_EXPORT_GLB\\" if rt.maxFilePath else "C:\\VR4_GLB\\"); self.edt_p_glb.setStyleSheet("background: white; color: black;") + self.btn_brw_glb = QtWidgets.QPushButton("..."); self.btn_brw_glb.setStyleSheet("background: #777; color: white; font-weight: bold;"); self.btn_brw_glb.clicked.connect(self.browse_glb_folder) + h_glb = QtWidgets.QHBoxLayout(); h_glb.addWidget(self.edt_p_glb); h_glb.addWidget(self.btn_brw_glb) + f_gen.addRow("Mapa:", self.cmb_bake_elem); f_gen.addRow("", self.chk_denoise); f_gen.addRow("Pasta JPG:", self.edt_p_bake); f_gen.addRow("Pasta GLB/ZIP:", h_glb) + l_gen.addWidget(self.btn_ref); l_gen.addLayout(f_gen); l_gen.addStretch(); self.tabs.addTab(t_gen, "🗂️ 1. Geral") + + # ABA 2 + t_geo = QtWidgets.QWidget(); l_geo = QtWidgets.QVBoxLayout(t_geo); f_geo = QtWidgets.QFormLayout() + self.chk_a_weld = QtWidgets.QCheckBox("Fundir Grupos na opção 'AUTO'"); self.chk_a_weld.setStyleSheet("color: #00FF00; font-weight: bold;"); self.chk_a_weld.setChecked(True) + self.spn_pct = QtWidgets.QDoubleSpinBox(); self.spn_pct.setStyleSheet("background: white; color: black;"); self.spn_pct.setRange(0.1, 100.0); self.spn_pct.setValue(40.0) + self.spn_min_poly = QtWidgets.QSpinBox(); self.spn_min_poly.setStyleSheet("background: white; color: black;"); self.spn_min_poly.setRange(50, 10000000); self.spn_min_poly.setValue(3000) + self.spn_max_sz = QtWidgets.QDoubleSpinBox(); self.spn_max_sz.setStyleSheet("background: white; color: black;"); self.spn_max_sz.setRange(10.0, 100000.0); self.spn_max_sz.setValue(800.0) + self.chk_a_slice = QtWidgets.QCheckBox("Incluir Fatiador no 'AUTO'"); self.chk_a_slice.setStyleSheet("color: white; font-weight: bold;") + f_geo.addRow(QtWidgets.QLabel("🧩 SOLDADOR:")); f_geo.addRow("", self.chk_a_weld); f_geo.addRow(QtWidgets.QLabel("🛠️ OTIMIZADOR:")); f_geo.addRow("Vertex %:", self.spn_pct); f_geo.addRow("Meta Polys:", self.spn_min_poly); f_geo.addRow(QtWidgets.QLabel("✂️ FATIADOR:")); f_geo.addRow("Cortar se >:", self.spn_max_sz); f_geo.addRow("", self.chk_a_slice) + l_geo.addLayout(f_geo); l_geo.addStretch(); self.tabs.addTab(t_geo, "📐 2. Geometria") + + # ABA 3 + t_tex = QtWidgets.QWidget(); l_tex = QtWidgets.QVBoxLayout(t_tex); f_tex = QtWidgets.QFormLayout() + self.spn_res = QtWidgets.QSpinBox(); self.spn_res.setStyleSheet("background: white; color: black;"); self.spn_res.setRange(128, 8192); self.spn_res.setValue(2048) + self.chk_a_res = QtWidgets.QCheckBox("Auto-Size"); self.chk_a_res.setStyleSheet("color: #FFD700; font-weight: bold;"); self.chk_a_res.setChecked(True) + self.s256 = QtWidgets.QDoubleSpinBox(); self.s256.setStyleSheet("background: white; color: black;"); self.s256.setRange(0.1, 100000.0); self.s256.setValue(100.0); self.s256.setPrefix("< 256px: ") + self.s512 = QtWidgets.QDoubleSpinBox(); self.s512.setStyleSheet("background: white; color: black;"); self.s512.setRange(0.1, 100000.0); self.s512.setValue(300.0); self.s512.setPrefix("< 512px: ") + self.s1024 = QtWidgets.QDoubleSpinBox(); self.s1024.setStyleSheet("background: white; color: black;"); self.s1024.setRange(0.1, 100000.0); self.s1024.setValue(600.0); self.s1024.setPrefix("< 1024px: ") + h_sz = QtWidgets.QHBoxLayout(); h_sz.addWidget(self.chk_a_res); h_sz.addWidget(self.s256); h_sz.addWidget(self.s512); h_sz.addWidget(self.s1024) + for w in [self.chk_a_res, self.s256, self.s512, self.s1024, self.spn_res]: w.valueChanged.connect(self.upd_res_col) if hasattr(w, 'valueChanged') else w.toggled.connect(self.upd_res_col) + f_tex.addRow("Res Máx (>):", self.spn_res); f_tex.addRow("Escala:", h_sz) + l_tex.addLayout(f_tex); l_tex.addStretch(); self.tabs.addTab(t_tex, "🎨 3. Textura") + + # ABA 4 + t_cld = QtWidgets.QWidget(); l_cld = QtWidgets.QVBoxLayout(t_cld); f_cld = QtWidgets.QFormLayout() + 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.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)) + 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) + l_cld.addLayout(f_cld); l_cld.addStretch(); self.tabs.addTab(t_cld, "☁️ 4. API Cloud") + layout.addWidget(self.tabs) + + self.btn_full = QtWidgets.QPushButton("🚀 EXECUTAR TUDO (AUTO)"); self.btn_full.setStyleSheet("background: #00A8FF; color: white; font-weight: bold; font-size: 15px; height: 50px; border-radius: 8px;") + self.btn_full.clicked.connect(self.run_full_auto); layout.addWidget(self.btn_full) + + self.tree = QtWidgets.QTreeWidget(); self.tree.setStyleSheet("background: white; color: black;"); self.tree.setHeaderLabels(["Objeto", "Status", "Polys", "Res", "Eixo"]) + self.tree.setColumnWidth(0, 260); self.tree.setColumnWidth(1, 140); self.tree.setColumnWidth(2, 70); self.tree.setColumnWidth(3, 70); self.tree.setColumnWidth(4, 70); self.tree.itemClicked.connect(self.on_item_click) + layout.addWidget(self.tree) + + self.pb = QtWidgets.QProgressBar(); self.pb.setAlignment(QtCore.Qt.AlignCenter); self.pb.setStyleSheet("QProgressBar { font-weight: bold; color: black; background: white; } QProgressBar::chunk { background: #00FF00; }"); self.pb.setFormat("Pronto") + layout.addWidget(self.pb) + + h_l = QtWidgets.QHBoxLayout() + self.b1 = QtWidgets.QPushButton("P1: Lista"); self.b1.setStyleSheet("background: #E0E0E0; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b1.clicked.connect(lambda: eng.load_selection(self)) + self.b2 = QtWidgets.QPushButton("P2: Fundir"); self.b2.setStyleSheet("background: #32CD32; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b2.clicked.connect(lambda: eng.attach_grouped_objects(self, False)) + self.b3 = QtWidgets.QPushButton("P3: Opt"); self.b3.setStyleSheet("background: #FF8C00; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b3.clicked.connect(lambda: eng.optimize_geometry(self)) + self.b4 = QtWidgets.QPushButton("P4: Fatiar"); self.b4.setStyleSheet("background: #8B008B; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b4.clicked.connect(lambda: eng.slice_large_objects(self, False)) + 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(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(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: cld.finalize_export(self, self.edt_p_bake.text(), self.edt_p_glb.text())) + for b in [self.b1, self.b2, self.b3, self.b4, self.b5, self.b6, self.b7]: h_l.addWidget(b) + layout.addLayout(h_l) + + self.btn_cancel = QtWidgets.QPushButton("CANCELAR / FECHAR"); self.btn_cancel.setStyleSheet("background: #FF0000; color: white; font-weight: bold; height: 40px; border-radius: 8px;"); self.btn_cancel.clicked.connect(self.cancel_all) + layout.addWidget(self.btn_cancel) + + def browse_glb_folder(self): + f = QtWidgets.QFileDialog.getExistingDirectory(self, "Selecione a Pasta GLB") + if f: self.edt_p_glb.setText(f + "\\") + + def browse_mp3(self): + f, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Selecione o Áudio", "", "MP3 (*.mp3)") + if f: self.e_mp3.setText(f); self.e_mp3.setStyleSheet("background: white; color: green; font-weight: bold;") + + def upd_res_col(self): eng.upd_res_col(self) + def get_processable_items(self): return eng.get_processable_items(self) + + def on_item_click(self, item, column): + o = rt.getNodeByName(item.text(0)) + if o: rt.select(o); rt.redrawViews() + + def run_full_auto(self): + self._is_cancelled = False + if not rt.execute("selection as array"): QtWidgets.QMessageBox.warning(self, "Aviso", "Selecione algo!"); return + if self.chk_a_weld.isChecked() and not self._is_cancelled: eng.attach_grouped_objects(self, True) + eng.load_selection(self) + if not self.get_processable_items(): return + if self.bake_items and not self._is_cancelled: eng.optimize_geometry(self) + if self.chk_a_slice.isChecked() and not self._is_cancelled: eng.slice_large_objects(self, True) + if not self._is_cancelled: eng.prepare_mesh(self) + if not self._is_cancelled: time.sleep(1.0); eng.process_bake_logic(self, True) + + def cancel_all(self): self._is_cancelled = True; self.close() \ No newline at end of file