diff --git a/BKP_deSegurança/install_vr4life.py b/BKP_deSegurança/install_vr4life.py new file mode 100644 index 0000000..f8608ca --- /dev/null +++ b/BKP_deSegurança/install_vr4life.py @@ -0,0 +1,61 @@ +import os +from pymxs import runtime as rt + +def install_vr4life_2026_fixed(): + try: + # 1. CAMINHOS + plugin_dir = os.path.dirname(os.path.realpath(__file__)).replace("\\", "/") + + # Onde o MNX foi copiado pelo seu instalador + pasta_macros = rt.getDir(rt.name("userMacros")) + pasta_enu = os.path.dirname(pasta_macros) + caminho_mnx = os.path.join(pasta_enu, "en-US", "UI", "vr4life.mnx").replace("\\", "/") + + # 2. REGISTRAR AS MACROS (Obrigatorio para o MNX funcionar) + # Usamos exatamente a categoria "Immerse Games" que está no seu MNX + macro_cmd = f""" +macroScript VR4Life_Launcher +category:"Immerse Games" +tooltip:"Abrir VR4Life" +( + on execute do python.executeFile "{plugin_dir}/run_vr4life.py" +) + +macroScript VR4Life_Update +category:"Immerse Games" +tooltip:"Atualizar VR4Life" +( + on execute do python.executeFile "{plugin_dir}/vr4life_updater.py" +) +""" + rt.execute(macro_cmd) + + # 3. CARREGAR O MNX VIA INTERFACE DE CONTEXTO (Abordagem Babylon 2026) + # Se o menuMan é undefined, usamos o carregamento de arquivo de configuração + setup_script = f""" + ( + local mnxPath = "{caminho_mnx}" + if (doesFileExist mnxPath) then ( + -- No 2026, tentamos carregar o arquivo de menus diretamente + -- sem passar pelas propriedades de "getMainMenuBar" + try ( + menuMan.loadMenuFile mnxPath + menuMan.updateMenuBar() + ) catch ( + -- Se falhar aqui, o Max carregará no próximo boot + -- pois o arquivo já está na pasta oficial de UI + ) + ) + ) + """ + rt.execute(setup_script) + + print(">> VR4Life: Macros registradas e MNX vinculado.") + return True + + except Exception as e: + print(f"Erro na instalação: {str(e)}") + return False + +if __name__ == "__main__": + install_vr4life_2026_fixed() \ No newline at end of file diff --git a/BKP_deSegurança/run_vr4life.py b/BKP_deSegurança/run_vr4life.py new file mode 100644 index 0000000..7f6f3bd --- /dev/null +++ b/BKP_deSegurança/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/BKP_deSegurança/v19 vr4life.ms b/BKP_deSegurança/v19 vr4life.ms new file mode 100644 index 0000000..afc3508 --- /dev/null +++ b/BKP_deSegurança/v19 vr4life.ms @@ -0,0 +1,510 @@ + +macroScript AutoBakeGLB_V45_2 +category:"Paulo Tools" +tooltip:"Bake v45.2" +buttonText:"BAKE v45.2" +( + -- Close existing dialogs + try(destroyDialog rolloutBakeGLB)catch() + + -- STRUCTURE TO HOLD DATA + struct BakeItemData ( node, listItem ) + + rollout rolloutBakeGLB "Auto-Bake Manager V45.2" width:600 height:860 + ( + -- HEADER + label lblTitle "VR4LIFE" align:#center height:30 css:"font-size:30px; font-weight:bold" + label lblVer "v45.2 (Fix Calc)" align:#center height:20 css:"font-size:12px; color:gray" offset:[0,-5] + + -- ==================================================================================== + -- 1. CONFIGURATION GROUP + -- ==================================================================================== + group "1. Output & Tools" + ( + edittext edtPath "Folder:" text:"" fieldWidth:360 align:#left across:2 + button btnBrowse "..." width:30 height:18 align:#right offset:[0,-2] + + edittext edtName "GLB Name:" text:"Scene_Exported" fieldWidth:200 align:#left across:2 + button btnOpenFolder "Open Folder" width:120 height:22 align:#right + + button btnFix "FIX GEOMETRY (Explode & Poly)" width:280 height:30 css:"background-color:#ffcccc; font-weight:bold" across:2 align:#left + button btnCheck "CHECK & ADD UNWRAP (Ch. 2)" width:280 height:30 align:#right + + spinner spnPolyPercent "Reduc. %:" range:[1.0, 100.0, 50.0] type:#float fieldWidth:50 across:3 align:#left offset:[0,4] + spinner spnPolyLimit "Max Verts:" range:[100, 1000000, 40000] type:#integer fieldWidth:60 align:#left offset:[0,4] + button btnReduce "REDUCE (ProOptimizer)" width:160 height:25 align:#right + + button btnInspect "INSPECT UV (Select 1)" width:580 height:30 align:#center + ) + + group "2. Render Configuration" + ( + radiobuttons rdoEngine labels:#("V-Ray", "Corona", "Arnold") default:1 columns:3 across:2 align:#left + spinner spnSize "Size:" range:[128, 8192, 512] type:#integer fieldwidth:50 align:#right + + spinner spnPasses "Pass Limit:" range:[1, 999, 5] type:#integer fieldwidth:50 across:2 align:#left + checkbox chkUseNative "Use F10 Settings (Ignore Limits)" checked:false offset:[0, 4] align:#right + + button btnConfig "Open F10 Config" width:280 height:25 across:2 align:#left + checkbox chkShowVFB "Show Render Window" checked:false offset:[0, 4] align:#right + + radiobuttons rdoFinalUV labels:#("UV 1 (Replace)", "UV 2 (Multi-UV)") default:1 columns:2 across:2 align:#left + checkbox chkAutoFlatten "Re-Flatten + Pack" checked:true align:#right + + checkbox chkOverwrite "Overwrite Existing Files" checked:false align:#center css:"color:red" + ) + + -- ==================================================================================== + -- 2. LIST MANAGER + -- ==================================================================================== + dotNetControl lvObjects "System.Windows.Forms.ListView" width:580 height:350 pos:[10, 350] + + -- button btnAddSel "POST SELECTION TO LIST" width:580 height:40 pos:[10, 710] css:"font-weight:bold" + -- button btnClear "CLEAR LIST" width:280 height:40 pos:[310, 710] + + progressbar pbProg "Progress:" color:green height:20 width:580 pos:[10, 760] + + button btnBakeChecked "BAKE CHECKED ITEMS" width:580 height:50 pos:[10, 790] css:"font-weight:bold; font-size:14px; background-color:#ccffcc" + button btnExportSel "EXPORT SELECTED (1+ Obj)" width:580 height:40 pos:[10, 850] + label lblStatus "Ready" pos:[10, 900] width:580 align:#center + + + -- LOCAL VARIABLES + -- ==================================================================================== + local bakeItems = #() + local userCancelled = false + local ignoreCheckEvent = false + + -- ==================================================================================== + -- HELPER FUNCTIONS + -- ==================================================================================== + + fn InitListView = ( + lvObjects.View = (dotNetClass "System.Windows.Forms.View").Details + lvObjects.FullRowSelect = true + lvObjects.GridLines = true + lvObjects.CheckBoxes = true + lvObjects.MultiSelect = true + + lvObjects.Columns.Add "Object Name" 280 + lvObjects.Columns.Add "Status" 120 + lvObjects.Columns.Add "Polys" 80 + ) + + fn UpdateItemStatus item status colorCode = ( + item.SubItems.Item[1].Text = status + item.ForeColor = colorCode + -- lvObjects.Refresh() -- Refreshing every item causes flicker, max handles it better usually + ) + + fn AutoPackUV obj channel = ( + try ( + max modify mode + select obj + modPanel.addModToSelection (Unwrap_UVW ()) ui:on + theMod = obj.modifiers[#Unwrap_UVW] + if theMod != undefined then ( + theMod.setMapChannel channel + theMod.flattenMap 45.0 #() 0.01 true 0 true true + theMod.setTVSubObjectMode 3 + theMod.selectFaces #{1..(theMod.numberPolygons())} + theMod.pack 1 0.001 true false true + collapseStack obj + ) + ) catch ( print ("Pack UV Error: " + obj.name) ) + ) + + fn ExportToGLB objs path = ( + if objs.count == 0 then return false + select objs + -- MANUAL EXPORT DIALOG (File Picker) + local filename = getSaveFileName caption:"Export GLB" types:"GL Transmission Format (*.glb)|*.glb|All|*.*" initialDir:path filename:edtName.text + + if filename != undefined then ( + try ( exportFile filename selectedOnly:true using:gltf_export; return true ) + catch ( messageBox "Export Cancelled or Error."; return false ) + ) + return false + ) + + fn CloseAnnoyingWindows = ( + try ( + local desktopHWND = windows.getDesktopHWND() + local children = windows.getChildrenHWND desktopHWND + local targets = #("Corona", "V-Ray", "Buffer", "Render", "Warning", "Error") + + for output in children do ( + local hwnd = output[1] + local title = output[5] + for t in targets do ( + if (matchPattern title pattern:("*" + t + "*") ignoreCase:true) then ( + try ( UIAccessor.CloseDialog hwnd ) catch () + -- try ( windows.sendMessage hwnd 0x0010 0 0 ) catch () -- Alternative Close + ) + ) + ) + ) catch () + ) + + -- ==================================================================================== + -- CORE LOGIC: BAKE ONE ITEM + -- ==================================================================================== + fn BakeOneObject objData = ( + obj = objData.node + item = objData.listItem + + -- Update UI + item.EnsureVisible() + UpdateItemStatus item "Baking..." (dotNetClass "System.Drawing.Color").OrangeRed + + -- Validate + if (isValidNode obj == false) then ( UpdateItemStatus item "Deleted" (dotNetClass "System.Drawing.Color").Red; return false) + + select obj + max modify mode + + try ( + ------------------------------------------------------------------------- + -- 1. PREPARE GEOMETRY & UV + ------------------------------------------------------------------------- + if (classof obj != Editable_Poly) then convertToPoly obj + + needNewUV = true + if (chkAutoFlatten.checked == false) then ( + if (polyop.getMapSupport obj 2) then needNewUV = false + else ( print "No UVs found on Channel 2. Auto-generating..." ) + ) + + if needNewUV then ( AutoPackUV obj 2; collapseStack obj ) + + ------------------------------------------------------------------------- + -- 2. PREPARE BAKE ELEMENTS + ------------------------------------------------------------------------- + obj.INodeBakeProperties.removeAllBakeElements() + be = undefined + fileExt = ".jpg" -- Default + + -- Engine Detection + curRen = renderers.current + isCorona = matchPattern (curRen as string) pattern:"*Corona*" + isVRay = matchPattern (curRen as string) pattern:"*V_Ray*" + isArnold = matchPattern (curRen as string) pattern:"*Arnold*" + + case rdoEngine.state of ( + 1: ( try ( be = VRayCompleteMap() ) catch ( ) ) + 2: ( try ( be = Corona_Beauty() ) catch ( try ( be = CShading_Beauty() ) catch ( ) ) ) + 3: ( be = CompleteMap() ) + ) + if be == undefined then be = CompleteMap() + + be.outputSzX = spnSize.value + be.outputSzY = spnSize.value + be.fileType = fileExt + -- SMART SKIP: Remove timestamp to allow stable file checking + be.filename = (edtPath.text + obj.name + "_Baked" + fileExt) + + fileExists = doesFileExist be.filename + skipRender = (fileExists) and (chkOverwrite.checked == false) + + if skipRender then ( + UpdateItemStatus item "Skipped (Exists)" (dotNetClass "System.Drawing.Color").Blue + ) else ( + -- PREPARE RENDER + obj.INodeBakeProperties.addBakeElement be + obj.INodeBakeProperties.bakeEnabled = true + obj.INodeBakeProperties.bakeChannel = 2 + + -- HARD ENFORCE CORONA LIMITS + if isCorona and (chkUseNative.checked == false) then ( + try(renderers.current.pass_limit = spnPasses.value; renderers.current.time_limit = 0; renderers.current.noise_level_limit = 0.0)catch() + ) + + wasCancelled = false + + -- RENDER EXECUTION + try ( + render rendertype:#bakeSelected vfb:chkShowVFB.checked progressBar:true quiet:true outputSize:[spnSize.value, spnSize.value] cancelled:&wasCancelled + if wasCancelled then userCancelled = true + ) catch ( + userCancelled = true + print "Render Error/Cancel Caught." + ) + + if userCancelled then ( UpdateItemStatus item "Cancelled" (dotNetClass "System.Drawing.Color").Red; return false ) + ) + + ------------------------------------------------------------------------- + -- 4. MATERIAL CREATION + ------------------------------------------------------------------------- + newMat = PhysicalMaterial() + newMat.name = (obj.name + "_Baked_Mat") + newMat.base_color = color 255 255 255 + newMat.roughness = 1.0 + newMat.emission = 0.0 -- Lit + newMat.emission_color = color 0 0 0 + + -- Wait for File Logic + fileFound = false + if (skipRender) then ( + fileFound = true -- File existed, we verified it before + ) else ( + -- We rendered, so we wait/verify + waitForFile = 0 + while (waitForFile < 50) and (fileFound == false) do ( + if doesFileExist be.filename then fileFound = true + else ( sleep 0.1; waitForFile += 1 ) + ) + ) + + if fileFound then ( + bmpTex = BitmapTexture filename:be.filename + if rdoFinalUV.state == 2 then bmpTex.coords.mapChannel = 2 else bmpTex.coords.mapChannel = 1 + newMat.base_color_map = bmpTex + showTextureMap newMat newMat.base_color_map on + ) else ( + print ("ERROR: Texture file not found: " + be.filename) + UpdateItemStatus item "Tex Missing" (dotNetClass "System.Drawing.Color").Red + return false + ) + + ------------------------------------------------------------------------- + -- 5. FINAL CLEANUP + ------------------------------------------------------------------------- + CloseAnnoyingWindows() -- Kill windows (V43) + sleep 1.0 -- Breathing room + + obj.material = newMat + + if rdoFinalUV.state == 1 then ( + channelInfo.CopyChannel obj 3 2 + channelInfo.PasteChannel obj 3 1 + channelInfo.ClearChannel obj 2 + collapseStack obj + ) else ( collapseStack obj ) + + UpdateItemStatus item "DONE" (dotNetClass "System.Drawing.Color").Green + return true + + ) catch ( + print ("Critical Error on Object: " + obj.name) + print (getCurrentException()) + UpdateItemStatus item "Error" (dotNetClass "System.Drawing.Color").Red + return false + ) + ) + + -- ==================================================================================== + -- EVENT HANDLERS + -- ==================================================================================== + on rolloutBakeGLB open do ( + defaultPath = maxFilePath + "_BAKED\\" + if maxFilePath == "" then defaultPath = "C:\\Temp_Bake\\" + edtPath.text = defaultPath + InitListView() + spnPasses.enabled = (not chkUseNative.checked) + ) + + on lvObjects ItemChecked arg do ( + if ignoreCheckEvent then return () + if arg.Item.Checked then ( + ignoreCheckEvent = true + -- Uncheck everyone else + for i = 0 to lvObjects.Items.Count-1 do ( + if lvObjects.Items.Item[i] != arg.Item then lvObjects.Items.Item[i].Checked = false + ) + ignoreCheckEvent = false + ) + ) + + on lvObjects ItemSelectionChanged arg do ( + if arg.IsSelected then ( + -- Find node in struct array + local selectedNode = undefined + for b in bakeItems do ( + if b.listItem == arg.Item then ( + selectedNode = b.node + exit + ) + ) + if isValidNode selectedNode then ( + select selectedNode + -- Optional: max zoomext selected + ) + ) + ) + + on chkUseNative changed state do ( spnPasses.enabled = (not state) ) + + on btnBrowse pressed do ( + newPath = getSavePath caption:"Save to:" initialDir:edtPath.text + if newPath != undefined then edtPath.text = (newPath + "\\") + ) + on btnOpenFolder pressed do ( ShellLaunch edtPath.text "" ) + on btnConfig pressed do ( renderSceneDialog.open() ) + + on btnFix pressed do ( + undo "Fix Geometry" on ( + local sel = selection as array + if sel.count == 0 then (messageBox "Select objects in viewport first!"; return false) + local groupHeads = for o in sel where isGroupHead o collect o + for g in groupHeads do ( if isValidNode g then explodeGroup g ) + + -- Re-select + max modify mode + local finalSel = selection as array + + -- AUTO-POPULATE LIST V24 (ADDITIVE) + local countAdded = 0 + + for obj in finalSel do ( + -- STRICT FILTER: Geometry Only, No Lights, No Cameras, No Helpers + if (isValidNode obj) and (superclassof obj == GeometryClass) and (classof obj != TargetObject) and (isGroupHead obj == false) then ( + + -- Always Ensure Poly (Re-Fix) + if canConvertTo obj Editable_Poly then convertToPoly obj + + -- Check for Duplicates + local isDuplicate = false + for b in bakeItems do ( if b.node == obj then isDuplicate = true ) + + if (not isDuplicate) then ( + li = lvObjects.Items.Add obj.name + li.SubItems.Add "Pending" + -- Use getPolygonCount for safety (returns array: #(faces, verts)) + local fCount = (getPolygonCount obj)[1] + li.SubItems.Add (fCount as string) + li.Checked = false + append bakeItems (BakeItemData node:obj listItem:li) + countAdded += 1 + ) + ) + ) + if countAdded > 0 then messageBox (countAdded as string + " Object(s) Added to List.") + else messageBox "Geometry Re-Fixed (No new items added)." + ) + ) + + on btnCheck pressed do ( + undo "Check UVs" on ( + objsToCheck = selection as array + local countAdded = 0 + max modify mode + for obj in objsToCheck do ( + if (superclassof obj == GeometryClass and isGroupHead obj == false) then ( + if (classof obj != Editable_Poly) then convertToPoly obj + + -- Check if modifier already exists (Ignore internal data support) + local hasModifier = false + for m in obj.modifiers do ( if classof m == Unwrap_UVW then hasModifier = true ) + + if (not hasModifier) then ( + select obj + modPanel.addModToSelection (Unwrap_UVW ()) ui:on + obj.modifiers[#Unwrap_UVW].setMapChannel 2 + countAdded += 1 + ) + ) + ) + if countAdded > 0 then messageBox ("Unwraps Added to Channel 2: " + countAdded as string) + else messageBox "Unwrap Modifier already present on selected object(s)." + ) + ) + + on btnReduce pressed do ( + undo "Reduce Polys" on ( + local sel = selection as array + if sel.count == 0 then (messageBox "Select objects first!"; return false) + + max modify mode + for obj in sel do ( + if (superclassof obj == GeometryClass) and (isGroupHead obj == false) then ( + if (classof obj != Editable_Poly) then convertToPoly obj + + select obj -- Ensure context + local mods = ProOptimizer() + addModifier obj mods + + mods.KeepUV = true -- Keep Textures! Keep UV Boundaries off by default for smooth + mods.OptimizationMode = 1 -- Protect Borders + mods.Calculate = true -- Trigger Calculation Explicitly + + -- Use Percentage AFTER Calculation + mods.VertexPercent = spnPolyPercent.value + + -- Update List Count if compatible + -- Force refresh of geometry for count + local newFCount = (getPolygonCount obj)[1] + + for b in bakeItems do ( + if b.node == obj then ( + b.listItem.SubItems.Item[2].Text = (newFCount as string) + ) + ) + ) + ) + messageBox "ProOptimizer Applied (User Settings) \u0026 List Updated." + ) + ) + + on btnInspect pressed do ( + if selection.count != 1 then (messageBox "Select EXACTLY ONE object."; return false) + obj = selection[1] + max modify mode + if (isGroupHead obj) then (messageBox "It is a group! Use Fix Geometry."; return false) + theMod = undefined + for m in obj.modifiers do ( if classof m == Unwrap_UVW then theMod = m ) + if theMod != undefined then ( + modPanel.setCurrentObject theMod + theMod.setMapChannel 2 + theMod.flattenMap 45.0 #() 0.01 true 0 true true + theMod.setTVSubObjectMode 3 + theMod.selectFaces #{1..(theMod.numberPolygons())} + theMod.pack 1 0.001 true false true + try ( theMod.edit() ) catch () + ) else ( messageBox "No Unwrap found. Use CHECK & ADD first." ) + ) + + on btnBakeChecked pressed do ( + userCancelled = false + + -- 1. Setup Render Limits GLOBALLY + curRen = renderers.current + isCorona = matchPattern (curRen as string) pattern:"*Corona*" + isVRay = matchPattern (curRen as string) pattern:"*V_Ray*" + isArnold = matchPattern (curRen as string) pattern:"*Arnold*" + + makeDir edtPath.text + + -- Store old logic similar to V19... + -- For brevity, we assume the BakeOneObject handles the per-object enforcement + -- But we should disable global time limits once here if not native + + undo off + ( + suspendEditing() + + for b in bakeItems do ( + if userCancelled then exit + if b.listItem.Checked then ( + -- Check if already done? Optional. For now we force re-bake if checked. + success = BakeOneObject b + ) + ) + + resumeEditing() + completeRedraw() + ) + + lblStatus.text = "Finished!" + ) + + on btnExportSel pressed do ( + if selection.count >= 1 then ( + local exportObj = selection as array + ExportToGLB exportObj edtPath.text + messageBox "Export Process Initiated." + ) else ( messageBox "Please select AT LEAST ONE object in the viewport." ) + ) + ) + createDialog rolloutBakeGLB height:930 +) diff --git a/BKP_deSegurança/version.txt b/BKP_deSegurança/version.txt new file mode 100644 index 0000000..e69de29 diff --git a/BKP_deSegurança/vr4life.mnx b/BKP_deSegurança/vr4life.mnx new file mode 100644 index 0000000..4c2aa8f --- /dev/null +++ b/BKP_deSegurança/vr4life.mnx @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/BKP_deSegurança/vr4life_cloud.py b/BKP_deSegurança/vr4life_cloud.py new file mode 100644 index 0000000..bae9d77 --- /dev/null +++ b/BKP_deSegurança/vr4life_cloud.py @@ -0,0 +1,124 @@ +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() + + ui.pb.setFormat("Preparando materiais VR (Multi-ID)..."); QtWidgets.QApplication.processEvents() + + 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): + 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") + except Exception as e: + d['item'].setText(1, "Erro Mat"); print(f"Erro Material em {d['name']}: {e}") + + if not fo and not ui._is_cancelled: + QtWidgets.QMessageBox.warning(ui, "Exportação Interrompida", "Nenhum objeto validado para exportação.") + 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) + + 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("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() + ) + ) + """) + + 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/BKP_deSegurança/vr4life_engine.py b/BKP_deSegurança/vr4life_engine.py new file mode 100644 index 0000000..f227c18 --- /dev/null +++ b/BKP_deSegurança/vr4life_engine.py @@ -0,0 +1,521 @@ +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 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)") + 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 inteiros...") + QtWidgets.QApplication.processEvents() + ms = """( + fn hasProtectedName nd = ( + local nm = toLower nd.name + if (matchPattern nm pattern:"*_mult*") or (matchPattern nm pattern:"*_catg*") or (matchPattern nm pattern:"*_mat*") do return true + for c in nd.children do ( if hasProtectedName c do return true ) + return false + ) + 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 and not (hasProtectedName o) collect o + + for h in heads do ( + local valid_cg = for c in h.children where superclassof c == GeometryClass and classof c != TargetObject collect c + 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 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 + )""" + try: + act = rt.execute(ms) + load_selection(ui) + if not from_auto: + if act > 0: QtWidgets.QMessageBox.information(ui, "Sucesso", f"{act} Grupos fundidos.") + 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 super_attach_objects(ui, from_auto=False): + ui.pb.setFormat("Super Solda VR...") + QtWidgets.QApplication.processEvents() + ms = """( + fn hasProtectedName nd = ( + local nm = toLower nd.name + if (matchPattern nm pattern:"*_mult*") or (matchPattern nm pattern:"*_catg*") or (matchPattern nm pattern:"*_mat*") do return true + for c in nd.children do ( if hasProtectedName c do return true ) + return false + ) + local sel = for o in selection where superclassof o == GeometryClass and classof o != TargetObject and not (hasProtectedName o) collect o + if sel.count > 1 do ( + local b = sel[1] + if not isKindOf b Editable_Poly do try(convertToPoly b)catch() + if isKindOf b Editable_Poly do ( + for i = 2 to sel.count do ( + local n = sel[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 = uniqueName "VR_Bloco_Fundido" + select b + ) + ) + sel.count + )""" + try: + act = rt.execute(ms) + load_selection(ui) + if not from_auto: + if act > 1: QtWidgets.QMessageBox.information(ui, "Sucesso", f"{act} objetos fundidos.") + else: QtWidgets.QMessageBox.information(ui, "Aviso", "Selecione pelo menos 2 objetos.") + except Exception as e: print("Erro Super 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 < 50: + d['item'].setText(1, "Geo Base") + else: + rt.select(o) + if not rt.isKindOf(o, rt.Editable_Poly): + rt.convertTo(o, rt.Editable_Poly) + + opt = rt.ProOptimizer() + rt.addModifier(o, opt) + opt.KeepTextures = True + opt.KeepNormals = True + opt.KeepBorders = True + opt.LockMat = True + opt.Calculate = True + + current_pct = 100.0 + p = 0 + while True: + if ui._is_cancelled: break + current_pct = current_pct * (sp / 100.0) + opt.VertexPercent = current_pct + rt.redrawViews() + QtWidgets.QApplication.processEvents() + + np = rt.getPolygonCount(o)[0] + p += 1 + d['item'].setText(2, f"{np:,}") + d['item'].setText(1, f"Opt ({p}x)") + QtWidgets.QApplication.processEvents() + + if np <= tg or np < 50 or p >= 20 or (np >= cp and p > 1): break + cp = np + + rt.collapseStack(o) + if not rt.isKindOf(o, rt.Editable_Poly): + rt.convertTo(o, rt.Editable_Poly) + + if not ui._is_cancelled: + d['item'].setText(2, f"{rt.getPolygonCount(o)[0]:,}") + d['item'].setText(1, f"Opt OK ({p}x)") + except Exception as e: + d['item'].setText(1, "Erro Opt") + print(f"Erro Opt {d['name']}: {e}") + + 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() + tgs = ui.get_processable_items() + tot = len(tgs) + ui.pb.setMaximum(tot) + ui.pb.setValue(0) + rt.execute("max modify mode") + + print("\n" + "="*50) + print("🔍 INICIANDO LOG: MULTI-UV (P4)") + print(f"-> Tamanho de corte (Threshold): {thr}") + print("="*50) + + for i, d in enumerate(tgs): + if ui._is_cancelled: break + ui.pb.setFormat(f"Multi-UV ({i+1}/{tot}): {d['name']}...") + ui.pb.setValue(i) + QtWidgets.QApplication.processEvents() + + print(f"\n--- Processando objeto: {d['name']} ---") + + # CÓDIGO CORRIGIDO: Tudo embrulhado num "fn" (função) nativo para o return funcionar. + ms = f"""( + fn applyMultiMatID objName thrVal = ( + local o = getNodeByName objName + if o == undefined do return -1 + if not isKindOf o Editable_Poly do convertToPoly o + + local dx = abs(o.max.x - o.min.x) + local dy = abs(o.max.y - o.min.y) + local dz = abs(o.max.z - o.min.z) + local md = amax #(dx, dy, dz) + + if md <= thrVal do ( + for f = 1 to (polyop.getNumFaces o) do polyop.setFaceMatID o f 1 + return 1 + ) + + local numChunks = (ceil (md / thrVal)) as integer + if numChunks > 10 do numChunks = 10 + + local axis = if md == dx then 1 else if md == dy then 2 else 3 + local minVal = if axis == 1 then o.min.x else if axis == 2 then o.min.y else o.min.z + local step = md / numChunks + + for f = 1 to (polyop.getNumFaces o) do ( + local center = polyop.getFaceCenter o f + local val = if axis == 1 then center.x else if axis == 2 then center.y else center.z + local id = (floor ((val - minVal) / step)) as integer + 1 + if id > numChunks do id = numChunks + if id < 1 do id = 1 + polyop.setFaceMatID o f id + ) + return numChunks + ) + try ( applyMultiMatID "{d['name']}" {thr} ) catch ( -2 ) + )""" + + try: + print("-> Executando MaxScript de Fatiamento Lógico...") + r = rt.execute(ms) + print(f"-> Resposta do MaxScript: {r}") + + if r == -1: + print("🚨 ERRO: Objeto não encontrado na cena pelo MaxScript.") + d['item'].setText(1, "Erro: Objeto") + elif r == -2: + print("🚨 ERRO: MaxScript falhou internamente (Topologia corrompida?).") + d['item'].setText(1, "Erro: Script") + elif r and int(r) > 1: + print(f"✅ SUCESSO: Objeto dividido em {int(r)} IDs!") + d['item'].setText(1, f"IDs: {int(r)}") + else: + print("-> Objeto menor que a Meta. Mantido como 1 ID.") + d['item'].setText(1, "1 ID (Normal)") + + except Exception as e: + d['item'].setText(1, "Erro Python") + print(f"🚨 ERRO CRÍTICO PYTHON no Multi-UV: {e}") + + ui.pb.setValue(tot) + ui.pb.setFormat("Multi-IDs Gerados!") + print("\n" + "="*50) + print("✅ FIM DO LOG MULTI-UV") + print("="*50 + "\n") + +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: + ms = f"""( + local o = getNodeByName "{d['name']}" + if not isKindOf o Editable_Poly do convertToPoly o + local m = Unwrap_UVW() + addModifier o m + m.setMapChannel 3 + m.setTVSubObjectMode 3 + local maxID = 1 + for f = 1 to (polyop.getNumFaces o) do ( + local id = polyop.getFaceMatID o f + if id > maxID do maxID = id + ) + for x = 1 to maxID do ( + m.selectByMatID x + local sel = m.getSelectedFaces() + if not sel.isEmpty do ( + m.flattenMap 45.0 #() 0.002 true 0 true false + m.pack 1 0.002 true false false + ) + ) + collapseStack o + )""" + rt.execute(ms) + if not rt.isKindOf(o, rt.Editable_Poly): + rt.convertTo(o, rt.Editable_Poly) + d['item'].setText(1, "UV Packed (C3)") + 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): + ui._is_cancelled = 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) + + print("\n" + "="*50) + print("🎬 INICIANDO LOG: MOTOR DE BAKE (P6)") + print(f"-> Pasta Alvo: {p_bk}") + print(f"-> Renderizador: {rnd}") + print("="*50) + + 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() + + print(f"\n--- Preparando Bake: {d['name']} ---") + + o = rt.getNodeByName(d['name']) + if not o: + print("🚨 ERRO: Objeto não encontrado.") + continue + + try: + # 1. Conta quantos IDs existem no objeto + ms_mid = 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)""" + max_id = rt.execute(ms_mid) + if not max_id: max_id = 1 + max_id = int(max_id) + print(f"-> Total de IDs a renderizar: {max_id}") + + 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) + + for mid in range(1, max_id + 1): + if ui._is_cancelled: break + print(f"\n-> Assando ID: {mid} de {max_id}") + + if max_id == 1: + t_jpg_id = os.path.join(p_bk, f"{d['name']}_B.jpg").replace("\\", "/") + else: + t_jpg_id = os.path.join(p_bk, f"{d['name']}_B_ID{mid}.jpg").replace("\\", "/") + + if os.path.exists(t_jpg_id): + d['item'].setText(1, f"Já existe ID{mid}") + print("-> Ficheiro já existe. Pulando.") + continue + + cr = ui.spn_res.value() + if 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_id = t_jpg_id.replace(".jpg", ".exr") if ext == ".exr" else t_jpg_id + + if max_id > 1: + print("-> Isolando UV do ID atual...") + # CÓDIGO CORRIGIDO 1: Embrulhado em 'fn' + ms_uv = f"""( + fn isolateUV currentMid = ( + global temp_uv_mod = Unwrap_UVW() + addModifier $ temp_uv_mod + temp_uv_mod.setMapChannel 3 + temp_uv_mod.setTVSubObjectMode 3 + local allF = #{{1..(polyop.getNumFaces $)}} + temp_uv_mod.selectByMatID currentMid + local tgtF = temp_uv_mod.getSelectedFaces() + local hideF = allF - tgtF + + if hideF.numberset > 0 do ( + temp_uv_mod.selectFaces hideF + temp_uv_mod.moveSelected [-10, -10, 0] + ) + return 1 + ) + try ( isolateUV {mid} ) catch ( 0 ) + )""" + res_uv = rt.execute(ms_uv) + if res_uv == 0: print("🚨 ERRO: O MaxScript não conseguiu manipular as UVs.") + + print(f"-> Disparando o Render: {t_rnd_id}") + # CÓDIGO CORRIGIDO 2: Embrulhado em 'fn' + ms_bake = f"""( + fn doBake fileOut szX szY fileExt cor den elem = ( + try(freeSceneBitmaps(); vfbControl #clearimage)catch() + if cor and den do try ( renderers.current.denoise_enable = true ) catch() + $.INodeBakeProperties.removeAllBakeElements() + + local be = (execute (elem + "()")) + be.outputSzX = szX + be.outputSzY = szY + be.fileType = fileExt + be.fileName = fileOut + + $.INodeBakeProperties.addBakeElement be + $.INodeBakeProperties.bakeEnabled = true + $.INodeBakeProperties.bakeChannel = 3 + render rendertype:#bakeSelected vfb:true quiet:true production:true outputfile:fileOut + return 1 + ) + try ( doBake "{t_rnd_id}" {cr} {cr} "{ext}" {str(i_cor).lower()} {str(u_den).lower()} "{el}" ) catch ( 0 ) + )""" + res_bk = rt.execute(ms_bake) + if res_bk == 0: print("🚨 ERRO: O Renderizador rejeitou o comando de Bake!") + + wt = 0 + while not os.path.exists(t_rnd_id) and wt < 60: + time.sleep(0.5) + wt += 0.5 + QtWidgets.QApplication.processEvents() + + if os.path.exists(t_rnd_id): + if i_vr and u_den: + print("-> Passando Denoise IA...") + c_in = t_rnd_id.replace("/", "\\") + c_out = t_rnd_id.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_id + 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_id}" 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_id); os.remove(c_out.replace("\\", "/")) + except: pass + + if max_id > 1: + print("-> Removendo modificador temporário de UV...") + rt.execute("try(deleteModifier $ globalvars.get #temp_uv_mod)catch()") + + if os.path.exists(t_jpg_id): + d['item'].setText(1, "Bake OK") + print("-> SUCESSO: Textura gravada no disco.") + else: + d['item'].setText(1, "Erro Conv") + print("🚨 ERRO: A textura não foi salva. Timeout?") + + if not ui._is_cancelled: + print("-> Restaurando UV final do objeto...") + rt.execute(f"""( local o = getNodeByName "{d['name']}"; try( ChannelInfo.CopyChannel o 3 3; ChannelInfo.PasteChannel o 3 1; collapseStack o )catch() )""") + + except Exception as e: + d['item'].setText(1, "Erro Render") + print(f"🚨 EXCEÇÃO PYTHON: {e}") + + ui.pb.setValue(tot) + ui.pb.setFormat("Bake Concluído!") + print("\n" + "="*50) + print("✅ FIM DO LOG DE BAKE") + print("="*50 + "\n") + + 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 + +def get_processable_items(ui): + valid_items = [] + for d in ui.bake_items: + obj = rt.getNodeByName(d['name']) + if obj: valid_items.append(d) + return valid_items \ No newline at end of file diff --git a/BKP_deSegurança/vr4life_ui.py b/BKP_deSegurança/vr4life_ui.py new file mode 100644 index 0000000..732f971 --- /dev/null +++ b/BKP_deSegurança/vr4life_ui.py @@ -0,0 +1,144 @@ +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(1050, 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.chk_a_super = QtWidgets.QCheckBox("Super Solda na opção 'AUTO'"); self.chk_a_super.setStyleSheet("color: #00FFFF; font-weight: bold;"); self.chk_a_super.setChecked(False) + 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(90.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 Multi-UV 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("", self.chk_a_super) + 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("✂️ MULTI-UV:")); f_geo.addRow("Criar IDs 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 e 4 mantêm-se iguais + 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") + + 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.b2_5 = QtWidgets.QPushButton("P2.5: Super Solda"); self.b2_5.setStyleSheet("background: #008080; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b2_5.clicked.connect(lambda: eng.super_attach_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: Multi-UV"); 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.b2_5, 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) + if self.chk_a_super.isChecked() and not self._is_cancelled: eng.super_attach_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 diff --git a/BKP_deSegurança/vr4life_updater.py b/BKP_deSegurança/vr4life_updater.py new file mode 100644 index 0000000..5b94abd --- /dev/null +++ b/BKP_deSegurança/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 diff --git a/Install_VR4Life_Menu.ms b/Install_VR4Life_Menu.ms new file mode 100644 index 0000000..a4615eb --- /dev/null +++ b/Install_VR4Life_Menu.ms @@ -0,0 +1,45 @@ +macroScript Launch_VR4Life +category:"VR4Life" +tooltip:"VR4Life Auto-Bake Plugin" +buttonText:"Ligar VR4Life" +( + on execute do ( + python.ExecuteFile @"Z:\Emmersive games\3dxMAX\vr4life-3dmax-plugin\run_vr4life.py" + ) +) + +macroScript Reload_VR4Life +category:"VR4Life" +tooltip:"Salvar Estado e Recarregar Plugin" +buttonText:"Recarregar VR4Life" +( + on execute do ( + python.Execute "import pymxs\ntry:\n pymxs.runtime.vr4_app.reload_plugin()\nexcept:\n pymxs.runtime.python.ExecuteFile(r'Z:\\Emmersive games\\3dxMAX\\vr4life-3dmax-plugin\\run_vr4life.py')" + ) +) + +( + local mainMenu = menuMan.getMainMenuBar() + + local oldMenu = menuMan.findMenu "VR4Life" + if oldMenu != undefined do ( + menuMan.unRegisterMenu oldMenu + ) + + local vr4Menu = menuMan.createMenu "VR4Life" + + local launchItem = menuMan.createActionItem "Launch_VR4Life" "VR4Life" + vr4Menu.addItem launchItem -1 + + local sep = menuMan.createSeparatorItem() + vr4Menu.addItem sep -1 + + local reloadItem = menuMan.createActionItem "Reload_VR4Life" "VR4Life" + vr4Menu.addItem reloadItem -1 + + local topLevelItem = menuMan.createSubMenuItem "VR4Life" vr4Menu + mainMenu.addItem topLevelItem -1 + + menuMan.updateMenuBar() + messageBox "Menu VR4Life atualizado! Agora a opcao de Recarregar foi transferida para ca." title:"VR4Life Toolbar" +) diff --git a/__pycache__/vr4life_cloud.cpython-310.pyc b/__pycache__/vr4life_cloud.cpython-310.pyc new file mode 100644 index 0000000..2cf8a41 Binary files /dev/null and b/__pycache__/vr4life_cloud.cpython-310.pyc differ diff --git a/__pycache__/vr4life_engine.cpython-310.pyc b/__pycache__/vr4life_engine.cpython-310.pyc new file mode 100644 index 0000000..bd82018 Binary files /dev/null and b/__pycache__/vr4life_engine.cpython-310.pyc differ diff --git a/__pycache__/vr4life_engine.cpython-313.pyc b/__pycache__/vr4life_engine.cpython-313.pyc new file mode 100644 index 0000000..e07d21e Binary files /dev/null and b/__pycache__/vr4life_engine.cpython-313.pyc differ diff --git a/__pycache__/vr4life_ui.cpython-310.pyc b/__pycache__/vr4life_ui.cpython-310.pyc new file mode 100644 index 0000000..d7dda97 Binary files /dev/null and b/__pycache__/vr4life_ui.cpython-310.pyc differ diff --git a/run_vr4life.py b/run_vr4life.py index 6101b85..0ebde46 100644 --- a/run_vr4life.py +++ b/run_vr4life.py @@ -11,5 +11,7 @@ import vr4life_ui 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 diff --git a/vr4_state.json b/vr4_state.json new file mode 100644 index 0000000..489f4c9 --- /dev/null +++ b/vr4_state.json @@ -0,0 +1 @@ +[{"name": "ETICHETTA", "status": "DONE V19", "polys": "5,966", "res": "256px", "id": "60.0"}, {"name": "Object003", "status": "DONE V19", "polys": "7,470", "res": "1024px", "id": "2394.0"}, {"name": "ETICHE_005", "status": "DONE V19", "polys": "2,977", "res": "256px", "id": "56.0"}, {"name": "IMBOTT_002", "status": "DONE V19", "polys": "6,628", "res": "1024px", "id": "2751.4"}, {"name": "Object005", "status": "DONE V19", "polys": "2,846", "res": "1024px", "id": "2456.7"}, {"name": "Object004", "status": "DONE V19", "polys": "2,962", "res": "1024px", "id": "1377.5"}, {"name": "Object006", "status": "DONE V19", "polys": "2,878", "res": "1024px", "id": "764.3"}, {"name": "CUCITURE", "status": "DONE V19", "polys": "936", "res": "256px", "id": "65.8"}] \ No newline at end of file diff --git a/vr4life_engine.py b/vr4life_engine.py index 9350cd1..b7051c8 100644 --- a/vr4life_engine.py +++ b/vr4life_engine.py @@ -11,6 +11,37 @@ def get_vdenoise_path(): if os.path.exists(p): return f'"{p}"' return "vdenoise.exe" +# Injeção segura na MAXScript Listener. O Python via PySide desvia stdout nativo, +# e chamar rt.execute() mil vezes destrói a memória (c0000005). +# Aqui criamos a função NO MAXSCRIPT APENAS UMA VEZ e a referenciamos. +rt.execute("""global vr4life_mprint_func +fn vr4life_mprint_func msg = ( + format "[VR4Life] %\\n" msg +) + +global vr4life_popup_killer +fn vr4life_popup_killer = ( + local hwnd = DialogMonitorOPS.GetWindowHandle() + if (hwnd != 0) do ( + local title = UIAccessor.GetWindowText hwnd + if (matchPattern title pattern:"*V-Ray*" ignoreCase:true) or (matchPattern title pattern:"*Corona*" ignoreCase:true) do ( + UIAccessor.PressButtonByName hwnd "Proceed" + UIAccessor.PressButtonByName hwnd "Yes" + UIAccessor.PressButtonByName hwnd "OK" + UIAccessor.PressButtonByName hwnd "Continue" + ) + ) + true +)""") + +import builtins +def mprint(*args, **kwargs): + try: + msg = " ".join([str(a) for a in args]) + rt.vr4life_mprint_func(msg) # Changed to call vr4life_mprint_func + except: pass +builtins.print = mprint # Redireciona todos os prints antigos deste arquivo pra Aba MAXScript F11 + def load_bake_elements(ui): ui.cmb_bake_elem.clear() found = ["CompleteMap", "VRayCompleteMap", "Corona_Beauty", "CShading_Beauty"] @@ -28,21 +59,66 @@ def load_bake_elements(ui): 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 )""" + mprint("======== [VR4Life] GATILHO DA INTERFACE: P1 [Lista] ========") + mprint("Lendo viewport para resgatar geometrias selecionadas...") + if not rt.execute("selection as array"): + mprint("ERRO: O usuario clicou em P1 mas nao ha nada selecionado.") + QtWidgets.QMessageBox.warning(ui, "Aviso", "Selecione objetos na viewport primeiro!") + return + + ms = """( + undo "Fix Geometry" on ( + local sel = selection as array + local groupHeads = for o in sel where isGroupHead o collect o + for g in groupHeads do ( if isValidNode g then explodeGroup g ) + + max modify mode + local finalSel = selection as array + local validObjs = #() + + for obj in finalSel do ( + if (isValidNode obj) and (superclassof obj == GeometryClass) and (classof obj != TargetObject) and (isGroupHead obj == false) then ( + if canConvertTo obj Editable_Poly then convertToPoly obj + appendIfUnique validObjs obj + ) + ) + validObjs + ) + )""" + 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] + + if not sel: + QtWidgets.QMessageBox.warning(ui, "Aviso", "Nenhuma geometria selecionada pós-explode.") + return + + existing_names = [d['name'] for d in ui.bake_items] + tl = [] + + for o in sel: + n = str(o.name) + if n not in existing_names: + poly_count = rt.getPolygonCount(o)[0] if rt.canConvertTo(o, rt.Editable_Poly) else 0 + tl.append({'name': n, 'poly': poly_count}) + + if not tl: + QtWidgets.QMessageBox.information(ui, "Info", "Seleção já existe na lista.") + return + 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.setFormat(f"Adicionados: {len(tl)} / Total: {len(ui.bake_items)}") ui.pb.setValue(0) ui.upd_res_col() + mprint(f"SUCESSO! {len(tl)} geometrias importadas para a tabela do UI com poligonos calculados.") + def attach_grouped_objects(ui, from_auto=False): ui.pb.setFormat("Fundindo grupos inteiros...") @@ -123,7 +199,9 @@ def super_attach_objects(ui, from_auto=False): except Exception as e: print("Erro Super Solda:", e) ui.pb.setValue(0); ui.pb.setFormat("Pronto") + def optimize_geometry(ui): + mprint("======== [VR4Life] GATILHO DA INTERFACE: P3 [Optimize] ========") sp = ui.spn_pct.value() tg = ui.spn_min_poly.value() tgs = ui.get_processable_items() @@ -189,312 +267,391 @@ def optimize_geometry(ui): 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() - tgs = ui.get_processable_items() - tot = len(tgs) - ui.pb.setMaximum(tot) - ui.pb.setValue(0) - rt.execute("max modify mode") - - print("\n" + "="*50) - print("🔍 INICIANDO LOG: MULTI-UV (P4)") - print(f"-> Tamanho de corte (Threshold): {thr}") - print("="*50) - - for i, d in enumerate(tgs): - if ui._is_cancelled: break - ui.pb.setFormat(f"Multi-UV ({i+1}/{tot}): {d['name']}...") - ui.pb.setValue(i) - QtWidgets.QApplication.processEvents() - - print(f"\n--- Processando objeto: {d['name']} ---") - - # CÓDIGO CORRIGIDO: Tudo embrulhado num "fn" (função) nativo para o return funcionar. - ms = f"""( - fn applyMultiMatID objName thrVal = ( - local o = getNodeByName objName - if o == undefined do return -1 - if not isKindOf o Editable_Poly do convertToPoly o - - local dx = abs(o.max.x - o.min.x) - local dy = abs(o.max.y - o.min.y) - local dz = abs(o.max.z - o.min.z) - local md = amax #(dx, dy, dz) - - if md <= thrVal do ( - for f = 1 to (polyop.getNumFaces o) do polyop.setFaceMatID o f 1 - return 1 - ) - - local numChunks = (ceil (md / thrVal)) as integer - if numChunks > 10 do numChunks = 10 - - local axis = if md == dx then 1 else if md == dy then 2 else 3 - local minVal = if axis == 1 then o.min.x else if axis == 2 then o.min.y else o.min.z - local step = md / numChunks - - for f = 1 to (polyop.getNumFaces o) do ( - local center = polyop.getFaceCenter o f - local val = if axis == 1 then center.x else if axis == 2 then center.y else center.z - local id = (floor ((val - minVal) / step)) as integer + 1 - if id > numChunks do id = numChunks - if id < 1 do id = 1 - polyop.setFaceMatID o f id - ) - return numChunks - ) - try ( applyMultiMatID "{d['name']}" {thr} ) catch ( -2 ) - )""" - - try: - print("-> Executando MaxScript de Fatiamento Lógico...") - r = rt.execute(ms) - print(f"-> Resposta do MaxScript: {r}") - - if r == -1: - print("🚨 ERRO: Objeto não encontrado na cena pelo MaxScript.") - d['item'].setText(1, "Erro: Objeto") - elif r == -2: - print("🚨 ERRO: MaxScript falhou internamente (Topologia corrompida?).") - d['item'].setText(1, "Erro: Script") - elif r and int(r) > 1: - print(f"✅ SUCESSO: Objeto dividido em {int(r)} IDs!") - d['item'].setText(1, f"IDs: {int(r)}") - else: - print("-> Objeto menor que a Meta. Mantido como 1 ID.") - d['item'].setText(1, "1 ID (Normal)") - - except Exception as e: - d['item'].setText(1, "Erro Python") - print(f"🚨 ERRO CRÍTICO PYTHON no Multi-UV: {e}") - - ui.pb.setValue(tot) - ui.pb.setFormat("Multi-IDs Gerados!") - print("\n" + "="*50) - print("✅ FIM DO LOG MULTI-UV") - print("="*50 + "\n") -def prepare_mesh(ui): +def prepare_mesh_v19(ui): + mprint("======== [VR4Life] GATILHO DA INTERFACE: P5 [Prepare UVP5] ========") 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.setFormat(f"UV v19 ({i+1}/{tot}): {d['name']}...") ui.pb.setValue(i) QtWidgets.QApplication.processEvents() + o = rt.getNodeByName(d['name']) if o: - rt.select(o) try: ms = f"""( - local o = getNodeByName "{d['name']}" - if not isKindOf o Editable_Poly do convertToPoly o - local m = Unwrap_UVW() - addModifier o m - m.setMapChannel 3 - m.setTVSubObjectMode 3 - local maxID = 1 - for f = 1 to (polyop.getNumFaces o) do ( - local id = polyop.getFaceMatID o f - if id > maxID do maxID = id - ) - for x = 1 to maxID do ( - m.selectByMatID x - local sel = m.getSelectedFaces() - if not sel.isEmpty do ( - m.flattenMap 45.0 #() 0.002 true 0 true false - m.pack 1 0.002 true false false - ) + local obj = getNodeByName "{d['name']}" + if obj != undefined do ( + max modify mode + select obj + local theMod = Unwrap_UVW() + modPanel.addModToSelection theMod ui:on + + -- Usando Channel 2 para o Bake (Corrigido o bug que gerava Channel 3 e deixava as UVs dessincronizadas do motor render) + theMod.setMapChannel 2 + + -- Lógica original do V19 reconstruída para "Pack Custom" real + theMod.flattenMap 45.0 #() 0.01 true 0 true true + theMod.setTVSubObjectMode 3 + theMod.selectFaces #{{1..(theMod.numberPolygons())}} + + -- Configurando o Menu "Arrange Elements" (Pack Custom) exatamente como no Print: + -- Usar Recursivo(1), Padding(0.001), Rescale=True(Normalize), Rotate=False, FillHoles=False + theMod.pack 1 0.001 true false false + + -- Não damos collapseStack a pedido do usuário! ) - collapseStack o )""" rt.execute(ms) - if not rt.isKindOf(o, rt.Editable_Poly): - rt.convertTo(o, rt.Editable_Poly) - d['item'].setText(1, "UV Packed (C3)") - except: + d['item'].setText(1, "UV V19 (C2)") + except Exception as e: d['item'].setText(1, "Erro UV") + print(f"Erro UV V19 em {d['name']}: {e}") ui.pb.setValue(tot) - ui.pb.setFormat("UV Pack Concluído!") + ui.pb.setFormat("UV Pack V19 Concluído!") + +def close_annoying_windows(): + ms = """( + try ( + local desktopHWND = windows.getDesktopHWND() + local children = windows.getChildrenHWND desktopHWND + local targets = #("Corona", "V-Ray", "Buffer", "Render", "Warning", "Error") + + for output in children do ( + local hwnd = output[1] + local title = output[5] + for t in targets do ( + if (matchPattern title pattern:("*" + t + "*") ignoreCase:true) then ( + try ( UIAccessor.CloseDialog hwnd ) catch () + ) + ) + ) + ) catch () + )""" + rt.execute(ms) + +def inspect_uv(ui): + tgs = ui.get_processable_items() + if len(tgs) != 1: + QtWidgets.QMessageBox.warning(ui, "Aviso", "Selecione exatamente UM objeto na lista para auditar a UV.") + return + + obj_name = tgs[0]['name'] + ms = f"""( + local o = getNodeByName "{obj_name}" + if o != undefined and isValidNode o do ( + select o + max modify mode + if o.modifiers[#Unwrap_UVW] != undefined then ( + modPanel.setCurrentObject o.modifiers[#Unwrap_UVW] + o.modifiers[#Unwrap_UVW].edit() + ) else ( + messageBox "O modificador Unwrap_UVW nao foi encontrado." + ) + ) + )""" + rt.execute(ms) -def process_bake_logic(ui, auto_export=False): +def process_bake_logic_v19(ui, auto_export=False): + print("\n" + "="*50) + print("🎬 INICIANDO LOG: MOTOR DE BAKE V19 PORTADO (P6)") ui._is_cancelled = 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() + if not tgs: return tot = len(tgs) ui.pb.setMaximum(tot) ui.pb.setValue(0) - print("\n" + "="*50) - print("🎬 INICIANDO LOG: MOTOR DE BAKE (P6)") - print(f"-> Pasta Alvo: {p_bk}") - print(f"-> Renderizador: {rnd}") - print("="*50) - + # Extract UI variables once + p_bk = ui.edt_p_bake.text().replace("\\", "/") + if not p_bk.endswith("/"): p_bk += "/" + if not os.path.exists(p_bk): os.makedirs(p_bk) + + try: res_val = int(ui.spn_res.value()) + except: res_val = 512 + + cv_passes = ui.spn_passes.value() + cv_native = "true" if ui.chk_native.isChecked() else "false" + + tgt_uv_state = 2 if ui.rdo_uv2.isChecked() else 1 # 1=Replace, 2=Multi-UV + for i, d in enumerate(tgs): - if ui._is_cancelled: break + if ui._is_cancelled: + print(f"\n🚨 Bake Cancelado pelo usuario.") + break + ui.pb.setFormat(f"Bake ({i+1}/{tot}): {d['name']}...") ui.pb.setValue(i) QtWidgets.QApplication.processEvents() - print(f"\n--- Preparando Bake: {d['name']} ---") + obj_name = d['name'] - o = rt.getNodeByName(d['name']) - if not o: - print("🚨 ERRO: Objeto não encontrado.") - continue + res_str = d['item'].text(3).replace("px", "") + sz = int(res_str) if res_str.isdigit() else res_val - try: - # 1. Conta quantos IDs existem no objeto - ms_mid = 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)""" - max_id = rt.execute(ms_mid) - if not max_id: max_id = 1 - max_id = int(max_id) - print(f"-> Total de IDs a renderizar: {max_id}") - - 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) - - for mid in range(1, max_id + 1): - if ui._is_cancelled: break - print(f"\n-> Assando ID: {mid} de {max_id}") - - if max_id == 1: - t_jpg_id = os.path.join(p_bk, f"{d['name']}_B.jpg").replace("\\", "/") - else: - t_jpg_id = os.path.join(p_bk, f"{d['name']}_B_ID{mid}.jpg").replace("\\", "/") - - if os.path.exists(t_jpg_id): - d['item'].setText(1, f"Já existe ID{mid}") - print("-> Ficheiro já existe. Pulando.") - continue - - cr = ui.spn_res.value() - if 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_id = t_jpg_id.replace(".jpg", ".exr") if ext == ".exr" else t_jpg_id + mprint(f"--- Iniciando script V19 portado para: {obj_name} ---") + + ms_v19_block = f"""( + fn runBake = ( + local objName = "{obj_name}" + local obj = getNodeByName objName - if max_id > 1: - print("-> Isolando UV do ID atual...") - # CÓDIGO CORRIGIDO 1: Embrulhado em 'fn' - ms_uv = f"""( - fn isolateUV currentMid = ( - global temp_uv_mod = Unwrap_UVW() - addModifier $ temp_uv_mod - temp_uv_mod.setMapChannel 3 - temp_uv_mod.setTVSubObjectMode 3 - local allF = #{{1..(polyop.getNumFaces $)}} - temp_uv_mod.selectByMatID currentMid - local tgtF = temp_uv_mod.getSelectedFaces() - local hideF = allF - tgtF - - if hideF.numberset > 0 do ( - temp_uv_mod.selectFaces hideF - temp_uv_mod.moveSelected [-10, -10, 0] + if (obj != undefined and isValidNode obj) then ( + select obj + max modify mode + try ( + ------------------------------------------------------------------------- + -- 1. PREPARE GEOMETRY & UV (V19 Logic 1:1) + ------------------------------------------------------------------------- + if (classof obj != Editable_Poly) then convertToPoly obj + + local needNewUV = true + if (polyop.getMapSupport obj 2) then needNewUV = false + else ( print "No UVs found on Channel 2. Auto-generating..." ) + + if needNewUV then ( + modPanel.addModToSelection (Unwrap_UVW ()) ui:on + local theMod = obj.modifiers[#Unwrap_UVW] + if theMod != undefined then ( + theMod.setMapChannel 2 + theMod.flattenMap 45.0 #() 0.01 true 0 true true + theMod.setTVSubObjectMode 3 + theMod.selectFaces #{{1..(theMod.numberPolygons())}} + theMod.pack 1 0.001 true false true + collapseStack obj ) - return 1 ) - try ( isolateUV {mid} ) catch ( 0 ) - )""" - res_uv = rt.execute(ms_uv) - if res_uv == 0: print("🚨 ERRO: O MaxScript não conseguiu manipular as UVs.") - - print(f"-> Disparando o Render: {t_rnd_id}") - # CÓDIGO CORRIGIDO 2: Embrulhado em 'fn' - ms_bake = f"""( - fn doBake fileOut szX szY fileExt cor den elem = ( - try(freeSceneBitmaps(); vfbControl #clearimage)catch() - if cor and den do try ( renderers.current.denoise_enable = true ) catch() - $.INodeBakeProperties.removeAllBakeElements() - local be = (execute (elem + "()")) - be.outputSzX = szX - be.outputSzY = szY + ------------------------------------------------------------------------- + -- 2. PREPARE BAKE ELEMENTS + ------------------------------------------------------------------------- + obj.INodeBakeProperties.removeAllBakeElements() + local be = undefined + local fileExt = ".jpg" + + local curRen = renderers.current + local isCorona = matchPattern (curRen as string) pattern:"*Corona*" + local isVRay = matchPattern (curRen as string) pattern:"*V_Ray*" + + if isVRay then ( try ( be = VRayCompleteMap() ) catch ( be = CompleteMap() ) ) + else if isCorona then ( try ( be = Corona_Beauty() ) catch ( try ( be = CShading_Beauty() ) catch ( be = CompleteMap() ) ) ) + else ( be = CompleteMap() ) + + if be == undefined then be = CompleteMap() + + be.outputSzX = {sz} + be.outputSzY = {sz} be.fileType = fileExt - be.fileName = fileOut - $.INodeBakeProperties.addBakeElement be - $.INodeBakeProperties.bakeEnabled = true - $.INodeBakeProperties.bakeChannel = 3 - render rendertype:#bakeSelected vfb:true quiet:true production:true outputfile:fileOut - return 1 - ) - try ( doBake "{t_rnd_id}" {cr} {cr} "{ext}" {str(i_cor).lower()} {str(u_den).lower()} "{el}" ) catch ( 0 ) - )""" - res_bk = rt.execute(ms_bake) - if res_bk == 0: print("🚨 ERRO: O Renderizador rejeitou o comando de Bake!") - - wt = 0 - while not os.path.exists(t_rnd_id) and wt < 60: - time.sleep(0.5) - wt += 0.5 - QtWidgets.QApplication.processEvents() - - if os.path.exists(t_rnd_id): - if i_vr and u_den: - print("-> Passando Denoise IA...") - c_in = t_rnd_id.replace("/", "\\") - c_out = t_rnd_id.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_id - 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_id}" 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_id); os.remove(c_out.replace("\\", "/")) - except: pass + local pBk = @"{p_bk}" + be.filename = (pBk + obj.name + "_Baked" + fileExt) + try ( be.fileOut = be.filename ) catch() - if max_id > 1: - print("-> Removendo modificador temporário de UV...") - rt.execute("try(deleteModifier $ globalvars.get #temp_uv_mod)catch()") - - if os.path.exists(t_jpg_id): - d['item'].setText(1, "Bake OK") - print("-> SUCESSO: Textura gravada no disco.") - else: - d['item'].setText(1, "Erro Conv") - print("🚨 ERRO: A textura não foi salva. Timeout?") - - if not ui._is_cancelled: - print("-> Restaurando UV final do objeto...") - rt.execute(f"""( local o = getNodeByName "{d['name']}"; try( ChannelInfo.CopyChannel o 3 3; ChannelInfo.PasteChannel o 3 1; collapseStack o )catch() )""") + local fileExists = doesFileExist be.filename + local skipRender = false -- Overwrite always enabled via UI integration + + if skipRender then ( + print "Skipped (Exists)" + ) else ( + obj.INodeBakeProperties.addBakeElement be + obj.INodeBakeProperties.bakeEnabled = true + obj.INodeBakeProperties.bakeChannel = 2 + + if isCorona and ({cv_native} == false) then ( + try(renderers.current.pass_limit = {cv_passes}; renderers.current.time_limit = 0; renderers.current.noise_level_limit = 0.0)catch() + ) - except Exception as e: - d['item'].setText(1, "Erro Render") - print(f"🚨 EXCEÇÃO PYTHON: {e}") + local wasCancelled = false + try ( + print "Ativando Silenciador de Popups (DialogMonitorOPS)..." + DialogMonitorOPS.unRegisterNotification id:#vr4_kill + DialogMonitorOPS.RegisterNotification vr4life_popup_killer id:#vr4_kill + DialogMonitorOPS.enabled = true + + if doesFileExist be.filename do ( try ( deleteFile be.filename ) catch() ) + + -- DESLIGA NA MARRA RENDER DISTRIBUIDO E TEST RESOLUTION DO V-RAY PARA PARAR DE ABORTAR O BAKE + local cR = renderers.current + if (matchPattern (cR as string) pattern:"*V_Ray*") do ( + try(cR.system_distributedRender = false)catch() + try(cR.image_testResolution = false)catch() + ) + + print "Disparando Render API V19..." + -- Fix 2024: explicit outputfile to guarantee disk writing and prevent black/missing files + render rendertype:#bakeSelected vfb:true progressBar:true quiet:true outputSize:[{sz}, {sz}] outputfile:be.filename cancelled:&wasCancelled + ) catch ( + print "Render Error/Cancel Caught." + wasCancelled = true + ) + + print "Desativando Silenciador de Popups..." + DialogMonitorOPS.enabled = false + DialogMonitorOPS.unRegisterNotification id:#vr4_kill + + if wasCancelled then return -1 + ) + + ------------------------------------------------------------------------- + -- 4. MATERIAL CREATION + ------------------------------------------------------------------------- + local newMat = PhysicalMaterial() + newMat.name = (obj.name + "_Baked_Mat") + newMat.base_color = color 255 255 255 + newMat.roughness = 1.0 + newMat.emission = 0.0 + + local fileFound = false + local waitForFile = 0 + while (waitForFile < 50) and (fileFound == false) do ( + if doesFileExist be.filename then fileFound = true + else ( sleep 0.1; waitForFile += 1 ) + ) + + if fileFound then ( + local bmpTex = BitmapTexture filename:be.filename + if {tgt_uv_state} == 2 then bmpTex.coords.mapChannel = 2 else bmpTex.coords.mapChannel = 1 + newMat.base_color_map = bmpTex + showTextureMap newMat newMat.base_color_map on + ) else ( + print ("ERROR: Texture file not found: " + be.filename) + return -2 + ) + + ------------------------------------------------------------------------- + -- 5. FINAL CLEANUP + ------------------------------------------------------------------------- + sleep 1.0 + obj.material = newMat + + if {tgt_uv_state} == 1 then ( + channelInfo.CopyChannel obj 3 2 + channelInfo.PasteChannel obj 3 1 + channelInfo.ClearChannel obj 2 + ) + + return 1 + ) catch ( + print ("Critical Error on Object: " + obj.name) + print (getCurrentException()) + return 0 + ) + ) else ( + print "Objeto Invalido" + return 0 + ) + ) + runBake() + )""" + + ret = rt.execute(ms_v19_block) + + if ret == 1: + d['item'].setText(1, "DONE V19") + d['item'].setForeground(1, QtGui.QColor(0, 255, 0)) + mprint(f"-> SUCESSO: Object {obj_name} Finalizado") + elif ret == -1: + d['item'].setText(1, "Cancelado") + ui._is_cancelled = True + elif ret == -2: + d['item'].setText(1, "Tex Missing") + d['item'].setForeground(1, QtGui.QColor(255, 0, 0)) + else: + d['item'].setText(1, "Error") + d['item'].setForeground(1, QtGui.QColor(255, 0, 0)) ui.pb.setValue(tot) - ui.pb.setFormat("Bake Concluído!") + ui.pb.setFormat("Bake V19 Concluído!") + print("\n" + "="*50) - print("✅ FIM DO LOG DE BAKE") + print("✅ FIM DO LOG DE BAKE V19") print("="*50 + "\n") if auto_export and not ui._is_cancelled: - cld.finalize_export(ui, p_bk, ui.edt_p_glb.text()) + export_glb_v19(ui) + +def export_glb_v19(ui): + mprint("\n" + "="*50) + mprint("🎬 INICIANDO MOTOR DE EXPORT P7: RAW GLB") + + p_glb = ui.edt_p_glb.text().replace("\\", "/") + if not p_glb.endswith("/"): p_glb += "/" + if not os.path.exists(p_glb): + try: os.makedirs(p_glb) + except: pass + + tgs = ui.get_processable_items() + if not tgs: + mprint("Aviso: Nenhum item valido na lista para exportar.") + return + + tgt_uv = 2 if ui.rdo_uv2.isChecked() else 1 + + if len(tgs) == 1: f_name = f"{tgs[0]['name']}_Export" + else: f_name = "VR4Life_Scene_Export" + full_path = p_glb + f_name + ".glb" + + names_str = "#(" + ",".join([f'"{d["name"]}"' for d in tgs]) + ")" + + mprint(f"-> Local de Saida: {full_path}") + mprint(f"-> Total de Objetos na Cena: {len(tgs)}") + + ms_export = f"""( + fn runExport = ( + local objNames = {names_str} + local clones = #() + local errored = false + + try ( + print "\\n-------------- DIAGNOSTICO DE EXPORTACAO --------------" + for nm in objNames do ( + local orig = getNodeByName nm + if orig != undefined do ( + local c = copy orig + c.name = orig.name + "_EXP" + convertToPoly c -- Forca o colapso de toda a arvore antes de manipular a UV + + -- APENAS CLONAR PARA EXPORTACAO, SEM MANIPULAR UV OU MATERIAIS + append clones c + ) + ) + + if clones.count > 0 then ( + print "Invocando Motor Nativo: gltf_export (selectedOnly:true)" + max select none + select clones + local res = exportFile @"{full_path}" #noPrompt selectedOnly:true using:gltf_export + print "Export Finalizado. Limpando cena..." + delete clones + return true + ) else ( + print "Nenhum arquivo para clonar!" + return false + ) + ) catch ( + print "Critical Error na preparacao do GLB" + print (getCurrentException()) + try(delete clones)catch() + return false + ) + ) + runExport() + )""" + + res = rt.execute(ms_export) + if res == True: + mprint("✅ SUCESSO: GLB Gerado com Texturas Embutidas (Modo Silencioso)!") + else: + mprint("🚨 ERRO: Falha na geracao. Vefique o log.") + + mprint("="*50 + "\n") def upd_res_col(ui): a_r = ui.chk_a_res.isChecked() diff --git a/vr4life_ui.py b/vr4life_ui.py index ecaa3d8..f303e55 100644 --- a/vr4life_ui.py +++ b/vr4life_ui.py @@ -1,4 +1,5 @@ import os +import json from pymxs import runtime as rt try: from PySide6 import QtWidgets, QtCore, QtGui except ImportError: from PySide2 import QtWidgets, QtCore, QtGui @@ -20,15 +21,17 @@ def get_max_window_safe(): 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(1050, 1380); self.bake_items = []; self._is_cancelled = False + self.setWindowTitle("VR4LIFE AUTO-BAKE V312 - MODULAR ENTERPRISE") + self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowMinimizeButtonHint | QtCore.Qt.WindowMaximizeButtonHint | QtCore.Qt.WindowCloseButtonHint) + 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() - rt.clearListener(); eng.load_bake_elements(self) + 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;") + # --- TOP HEADER --- + lbl_t = QtWidgets.QLabel("VR4LIFE - V312 (Modular) ( ultima alteração 2:56 AM 11-03- 2026)"); 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() @@ -39,11 +42,18 @@ class AutoBakeManager(QtWidgets.QDialog): 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.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.") 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) + self.rdo_uv1 = QtWidgets.QRadioButton("UV 1 (Substituir)"); self.rdo_uv1.setStyleSheet("color: white; font-weight: bold;") + self.rdo_uv2 = QtWidgets.QRadioButton("UV 2 (Multi-UV)"); self.rdo_uv2.setStyleSheet("color: white; font-weight: bold;"); self.rdo_uv2.setChecked(True) + h_uv = QtWidgets.QHBoxLayout(); h_uv.addWidget(self.rdo_uv1); h_uv.addWidget(self.rdo_uv2) + + f_gen.addRow("Mapa:", self.cmb_bake_elem); f_gen.addRow("", self.chk_denoise); f_gen.addRow("Boost de Luz:", self.spn_light_boost) + f_gen.addRow("Destino UV:", h_uv) + 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 @@ -68,6 +78,13 @@ class AutoBakeManager(QtWidgets.QDialog): 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) + + self.spn_passes = QtWidgets.QSpinBox(); self.spn_passes.setStyleSheet("background: white; color: black;"); self.spn_passes.setRange(1, 9999); self.spn_passes.setValue(5) + self.chk_native = QtWidgets.QCheckBox("Usar F10 Nativ."); self.chk_native.setStyleSheet("color: white; font-weight: bold;"); self.chk_native.setChecked(False) + self.btn_f10 = QtWidgets.QPushButton("⚙️ F10 Config"); self.btn_f10.setStyleSheet("background: #555; color: white;"); self.btn_f10.clicked.connect(lambda: rt.execute("max render options")) + h_cor = QtWidgets.QHBoxLayout(); h_cor.addWidget(self.spn_passes); h_cor.addWidget(self.chk_native); h_cor.addWidget(self.btn_f10) + f_tex.addRow("Corona Limit (Pass):", h_cor) + 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() @@ -93,27 +110,58 @@ class AutoBakeManager(QtWidgets.QDialog): 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) + # ====== RESTAURAR ESTADO APÓS RECARREGAR (PERSISTENTE NA CENA) ====== + rt.execute("persistent global VR4Life_SceneListState") + + state_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "vr4_state.json") + saved_items = [] + + try: + # 1. Tenta recuperar da memória da cena atual + state_str = rt.VR4Life_SceneListState + if state_str and isinstance(state_str, str): + saved_items = json.loads(state_str) + # 2. Fallback pro arquivo local antigo + elif os.path.exists(state_file): + with open(state_file, 'r', encoding='utf-8') as f: + saved_items = json.load(f) + os.remove(state_file) + + for d in saved_items: + it = QtWidgets.QTreeWidgetItem([d.get('name', ''), d.get('status', ''), d.get('polys', ''), d.get('res', ''), d.get('id', '')]) + self.tree.addTopLevelItem(it) + self.bake_items.append({'name': d.get('name', ''), 'item': it}) + except Exception as e: + print("Aviso: Falha ao restaurar estado da UI da Cena:", e) + 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.b2_5 = QtWidgets.QPushButton("P2.5: Super Solda"); self.b2_5.setStyleSheet("background: #008080; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b2_5.clicked.connect(lambda: eng.super_attach_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.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.b2_5 = QtWidgets.QPushButton("P2.5: Super Solda"); self.b2_5.setStyleSheet("background: #008080; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b2_5.clicked.connect(lambda: eng.super_attach_objects(self, False)) - self.b4 = QtWidgets.QPushButton("P4: Multi-UV"); 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.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.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())) + 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.b2, self.b2_5, self.b3, self.b4, self.b5, self.b6, self.b7]: h_l.addWidget(b) + for b in [self.b1, self.b3, self.b5, self.b6, self.b7, self.b8, self.b_clear]: 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) + + 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) layout.addWidget(self.btn_cancel) + def clear_list(self): + self.tree.clear() + self.bake_items.clear() + self.pb.setFormat("Lista Vazia") + self.pb.setValue(0) + def browse_glb_folder(self): f = QtWidgets.QFileDialog.getExistingDirectory(self, "Selecione a Pasta GLB") if f: self.edt_p_glb.setText(f + "\\") @@ -132,13 +180,53 @@ class AutoBakeManager(QtWidgets.QDialog): 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) - if self.chk_a_super.isChecked() and not self._is_cancelled: eng.super_attach_objects(self, True) + # if self.chk_a_weld.isChecked() and not self._is_cancelled: eng.attach_grouped_objects(self, True) + # if self.chk_a_super.isChecked() and not self._is_cancelled: eng.super_attach_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 + if not self._is_cancelled: eng.prepare_mesh_v19(self) + + def save_persistent_state(self): + state_data = [] + for d in self.bake_items: + it = d['item'] + state_data.append({ + 'name': d['name'], + 'status': it.text(1), + 'polys': it.text(2), + 'res': it.text(3), + 'id': it.text(4) + }) + + try: + # Salva na Cena do 3ds Max! + json_str = json.dumps(state_data) + rt.execute("persistent global VR4Life_SceneListState") + rt.VR4Life_SceneListState = json_str + + # Fallback local (pra garantir) + state_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "vr4_state.json") + with open(state_file, 'w', encoding='utf-8') as f: + json.dump(state_data, f) + except Exception as e: + print("Erro ao salvar o estado da UI na cena:", e) + + def closeEvent(self, event): + self.save_persistent_state() + try: super().closeEvent(event) + except: pass + + def reload_plugin(self): + self._is_cancelled = True + self.save_persistent_state() + + self.close() + p = os.path.join(os.path.dirname(os.path.realpath(__file__)), "run_vr4life.py") + rt.execute(f"python.ExecuteFile @\"{p}\"") + + def cancel_all(self): + self._is_cancelled = True + self.pb.setFormat("⚠️ OPERAÇÃO CANCELADA PELO USUÁRIO") + self.pb.setStyleSheet("QProgressBar { font-weight: bold; color: white; background: #FF0000; }") \ No newline at end of file