@ -3,122 +3,255 @@ from pymxs import runtime as rt
try : from PySide6 import QtWidgets
try : from PySide6 import QtWidgets
except ImportError : from PySide2 import QtWidgets
except ImportError : from PySide2 import QtWidgets
import urllib . request
import urllib . parse
import json
def mock_connect_api ( ui ) :
def mock_connect_api ( ui ) :
if len ( ui . e_hash . text ( ) ) < 5 : QtWidgets . QMessageBox . warning ( ui , " Acesso Negado " , " Hash inválida. " ) ; return
hash_txt = ui . e_hash . text ( ) . strip ( )
ui . b_conn . setText ( " Conectando... " ) ; QtWidgets . QApplication . processEvents ( ) ; time . sleep ( 1 )
if len ( hash_txt ) < 5 :
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; " )
QtWidgets . QMessageBox . warning ( ui , " Acesso Negado " , " Hash inválida. " )
ui . c_chn . clear ( ) ; ui . c_chn . addItems ( [ " Canal: Imóveis Virtuais " , " Canal: Showroom Zombisco " ] )
return
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 ( " Conectando... " ) ; QtWidgets . QApplication . processEvents ( )
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 )
# Faz chamada real para a API
url = " https://api.vr4life.com/public/listaCanais "
req_data = urllib . parse . urlencode ( { " tx_hash_login_externo " : hash_txt } ) . encode ( ' utf-8 ' )
req = urllib . request . Request ( url , data = req_data , headers = { ' Content-Type ' : ' application/x-www-form-urlencoded ' } )
try :
with urllib . request . urlopen ( req , timeout = 10 ) as response :
res_body = response . read ( ) . decode ( ' utf-8 ' )
canais = json . loads ( res_body )
ui . c_chn . clear ( )
# Espera receber lista: [{"nm_canal": "...", "tx_hash_canal": "..."}, ...]
if isinstance ( canais , list ) and len ( canais ) > 0 :
for c in canais :
n_c = str ( c . get ( " nm_canal " , " Canal Desconhecido " ) )
h_c = str ( c . get ( " tx_hash_canal " , " " ) )
ui . c_chn . addItem ( n_c , h_c )
else :
raise Exception ( " Nenhum canal encontrado para este Hash. " )
except Exception as e :
ui . b_conn . setText ( " 🔄 Tentar Novamente " )
QtWidgets . QMessageBox . critical ( ui , " Erro de API " , f " Falha ao conectar: { str ( e ) } " )
return
def on_post_changed ( ui , idx ) :
# Ativa campos após sucesso
if idx == 0 :
for w in [ ui . c_chn , ui . rdo_add , ui . rdo_edt , ui . e_tit , ui . e_mp3 , ui . b_mp3 ] :
ui . e_tit . setText ( " " ) ; ui . e_tit . setPlaceholderText ( " Título do projeto... " )
w . setEnabled ( True )
ui . chk_tmb . setChecked ( True ) ; ui . chk_tmb . setEnabled ( False ) ; ui . chk_tmb . setStyleSheet ( " color: #00FF00; font-weight: bold; " )
w . setStyleSheet ( " background: white; color: black; font-weight: bold; " )
else :
ui . e_tit . setText ( ui . c_pst . currentText ( ) . replace ( " Editar: " , " " ) )
ui . b_up . setEnabled ( True )
ui . chk_tmb . setEnabled ( True ) ; ui . chk_tmb . setStyleSheet ( " color: white; font-weight: bold; " )
ui . b_conn . setText ( " ✅ Conectado! " ) ; ui . b_conn . setStyleSheet ( " background: #000; color: #0F0; font-weight: bold; border: 1px solid #0F0; " )
# Auto-carrega postagens do primeiro canal
fetch_posts_for_channel ( ui )
def finalize_export ( ui , p_bk , p_glb ) :
def f etch_posts_for_channel( ui ) :
if not os . path . exists ( p_glb ) : os . makedirs ( p_glb )
ui . c_pst . blockSignals ( True )
fo = [ ] ; rt . execute ( " max modify mode " ) ; tgs = ui . get_processable_items ( )
ui. c_pst . clear ( )
ui . pb . setFormat ( " Preparando materiais VR (Multi-ID)... " ) ; QtWidgets . QApplication . processEvents ( )
hash_txt = ui . e_hash . text ( ) . strip ( )
idx = ui . c_chn . currentIndex ( )
has_posts = False
for d in tgs :
if idx > = 0 :
if ui . _is_cancelled : break
ch_hash = ui . c_chn . itemData ( idx )
if " Bake " in d [ ' item ' ] . text ( 1 ) or " Já existe " in d [ ' item ' ] . text ( 1 ) or " Mat " in d [ ' item ' ] . text ( 1 ) :
print ( f " [DEBUG UI] Canal Index { idx } . Valor Extraído do ItemData: ' { ch_hash } ' " )
if ch_hash and str ( ch_hash ) . strip ( ) :
ch_hash = str ( ch_hash ) . strip ( )
url = " https://api.vr4life.com/public/listaPostagemCanal "
dict_data = { " tx_hash_login_externo " : hash_txt , " tx_hash_canal " : ch_hash }
req_data = urllib . parse . urlencode ( dict_data ) . encode ( ' utf-8 ' )
print ( f " \n [DEBUG API] Chamando: { url } " )
print ( f " [DEBUG API] Payload (FormData): { dict_data } " )
req = urllib . request . Request ( url , data = req_data , headers = { ' Content-Type ' : ' application/x-www-form-urlencoded ' } )
try :
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) """ )
with urllib . request . urlopen ( req , timeout = 10 ) as response :
raw_res = response . read ( ) . decode ( ' utf-8 ' )
if max_id > 1 :
posts = json . loads ( raw_res )
# CONSTRUTOR DE MULTI-MATERIAL (Para objetos que foram processados pelo P4: Multi-UV)
if isinstance ( posts , list ) and len ( posts ) > 0 :
ms_mat = f """ (
has_posts = True
local o = getNodeByName " {d['name']} "
for p in posts :
local mm = MultiMaterial numsubs : { int ( max_id ) } name : ( o . name + " _MultiMat " )
t_p = p . get ( ' tx_titulo ' , ' Sem Título ' )
for i = 1 to { int ( max_id ) } do (
h_p = p . get ( ' hash_postagem ' , ' ' )
local ip = @ " {p_bk} / {d['name']} _B_ID " + ( i as string ) + " .jpg "
print ( f " [DEBUG API] Encontrou Postagem: ' { t_p } ' | Hash: { h_p } " )
if doesFileExist ip do (
ui . c_pst . addItem ( t_p , h_p )
local m = PhysicalMaterial name : ( o . name + " _MatID_ " + ( i as string ) )
except urllib . error . HTTPError as e :
m . base_color_map = BitmapTexture filename : ip
err_body = e . read ( ) . decode ( ' utf-8 ' )
m . roughness = 1.0 ; m . metalness = 0.0 ; try ( m . reflectivity = 0.0 ; m . reflection_weight = 0.0 ) catch ( )
QtWidgets . QMessageBox . critical ( ui , f " Erro HTTP { e . code } " , f " O Servidor RECUSOU a requisição. \n \n Resposta: \n { err_body } " )
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 :
except Exception as e :
d [ ' item ' ] . setText ( 1 , " Erro Mat " ) ; print ( f " Erro Material em { d [ ' name ' ] } : { e } " )
QtWidgets . QMessageBox . warning ( ui , " Aviso de Rede " , f " Falha na comunicação ou falha lendo o JSON de postagens: \n \n { e } \n \n (A API pode ter retornado HTML ao invés de JSON puro?) " )
else :
QtWidgets . QMessageBox . warning ( ui , " Aviso " , " O Hash deste canal não foi preenchido. " )
ui . c_pst . blockSignals ( False )
# Se não retornou posts, forçar modo "Novo" e bloquear edição
if not has_posts :
ui . rdo_add . setChecked ( True )
ui . rdo_edt . setEnabled ( False )
else :
ui . rdo_edt . setEnabled ( True )
on_post_changed ( ui )
def on_post_changed ( ui ) :
if ui . rdo_add . isChecked ( ) :
ui . c_pst . setEnabled ( False )
ui . c_pst . setStyleSheet ( " background: #CCC; color: black; font-weight: normal; " )
ui . e_tit . setText ( " " ) ; ui . e_tit . setPlaceholderText ( " Título do NOVO projeto... " )
ui . b_tmb . setEnabled ( False )
else :
ui . c_pst . setEnabled ( True )
ui . c_pst . setStyleSheet ( " background: white; color: black; font-weight: bold; " )
if ui . c_pst . count ( ) > 0 :
ui . e_tit . setText ( ui . c_pst . currentText ( ) )
ui . b_tmb . setEnabled ( True )
def finalize_export ( ui , p_glb , f_name ) :
# Transição de abas após o término do Exportador V19
ui . tabs . setCurrentIndex ( 3 )
def generate_thumbnail ( ui ) :
p_glb = ui . edt_p_glb . text ( ) . replace ( " \\ " , " / " )
if not p_glb . endswith ( " / " ) : p_glb + = " / "
tgs = ui . get_processable_items ( )
if len ( tgs ) == 1 : sn = f " { tgs [ 0 ] [ ' name ' ] } _Export "
else : sn = " VR4Life_Scene_Export "
tp = os . path . join ( p_glb , f " { sn } _Thumb.jpg " ) . replace ( " \\ " , " / " )
ui . pb . setFormat ( " 📸 Tirando Snapshot da Cena... " ) ; QtWidgets . QApplication . processEvents ( )
rt . execute ( f """ ( local c = getActiveCamera(); if c == undefined do ( local ac = for cam in cameras where (classof cam != TargetObject) collect cam; if ac.count > 0 do c = ac[1] ); try ( if c != undefined then ( render camera:c outputwidth:1280 outputheight:720 outputfile:@ " { tp } " vfb:false quiet:true ) else ( render outputwidth:1280 outputheight:720 outputfile:@ " { tp } " vfb:false quiet:true ) ) catch() ) """ )
ui . pb . setFormat ( " ✅ Thumbnail Gerado e Salvo! " ) ; ui . pb . setValue ( 100 )
try : os . startfile ( p_glb )
except : pass
def upload_to_cloud ( ui ) :
p_glb = ui . edt_p_glb . text ( ) . replace ( " \\ " , " / " )
if not p_glb . endswith ( " / " ) : p_glb + = " / "
tgs = ui . get_processable_items ( )
if len ( tgs ) == 1 : sn = f " { tgs [ 0 ] [ ' name ' ] } _Export "
else : sn = " VR4Life_Scene_Export "
oz = os . path . join ( p_glb , f " { sn } .zip " ) . replace ( " \\ " , " / " )
tp = os . path . join ( p_glb , f " { sn } _Thumb.jpg " ) . replace ( " \\ " , " / " )
if not os . path . exists ( oz ) :
try :
zip_files = [ os . path . join ( p_glb , f ) for f in os . listdir ( p_glb ) if f . lower ( ) . endswith ( ' .zip ' ) ]
if zip_files :
oz = max ( zip_files , key = os . path . getmtime )
base_zip_name = os . path . basename ( oz ) . replace ( " .zip " , " " )
alt_tp = os . path . join ( p_glb , f " { base_zip_name } _Thumb.jpg " )
if os . path . exists ( alt_tp ) : tp = alt_tp
except Exception as e : print ( f " Erro ao buscar zip recente: { e } " )
if not fo and not ui . _is_cancelled :
if not os . path . exists ( oz ) :
QtWidgets . QMessageBox . warning ( ui , " Exportação Interrompida " , " Nenhum objeto validado para exportação. " )
QtWidgets . QMessageBox . warning ( ui , " Aviso " , f " Nenhum arquivo ZIP encontrado na pasta: \n { p_glb } \n \n Faça o Bake primeiro! " )
return
ui . pb . setFormat ( f " 📡 API: Sincronizando com a Nuvem... " ) ; QtWidgets . QApplication . processEvents ( )
hx_login = ui . e_hash . text ( ) . strip ( )
ch_idx = ui . c_chn . currentIndex ( )
hx_canal = ui . c_chn . itemData ( ch_idx ) if ch_idx > = 0 else " "
titulo = ui . e_tit . text ( ) . strip ( )
hx_postagem = " "
if ui . rdo_edt . isChecked ( ) :
pst_idx = ui . c_pst . currentIndex ( )
if pst_idx > = 0 : hx_postagem = ui . c_pst . itemData ( pst_idx )
if not hx_login or not hx_canal or not titulo :
QtWidgets . QMessageBox . warning ( ui , " Campos Inválidos " , " Por favor conecte-se à API, selecione um Canal e digite um Título antes de enviar! " )
ui . pb . setFormat ( " Pronto " ) ; ui . pb . setValue ( 0 )
ui . pb . setFormat ( " Pronto " ) ; ui . pb . setValue ( 0 )
return
return
if fo and not ui . _is_cancelled :
import mimetypes
rt . select ( fo ) ; sn = rt . maxFileName . split ( " . " ) [ 0 ] if rt . maxFileName else " Cena_VR4LIFE "
boundary = ' ----WebKitFormBoundaryVR4PluginX1A '
og = os . path . join ( p_glb , f " { sn } .glb " ) . replace ( " \\ " , " / " ) ; oz = os . path . join ( p_glb , f " { sn } .zip " ) . replace ( " \\ " , " / " )
data_parts = [ ]
tp = os . path . join ( p_glb , f " { sn } _Thumb.jpg " ) . replace ( " \\ " , " / " ) ; mp = ui . e_mp3 . text ( ) ; hm = os . path . exists ( mp )
def add_text_field ( name , value ) :
data_parts . append ( f ' -- { boundary } \r \n ' . encode ( ' utf-8 ' ) )
data_parts . append ( f ' Content-Disposition: form-data; name= " { name } " \r \n \r \n ' . encode ( ' utf-8 ' ) )
data_parts . append ( f ' { value } \r \n ' . encode ( ' utf-8 ' ) )
to = False
def add_file_field ( name , filename , filepath ) :
if ui . chk_tmb . isChecked ( ) and ui . chk_tmb . isEnabled ( ) :
try :
ui . pb . setFormat ( " 📸 Thumbnail (1280x720)... " ) ; QtWidgets . QApplication . processEvents ( )
mime_type = mimetypes . guess_type ( filepath ) [ 0 ] or ' application/octet-stream '
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() ) """ )
with open ( filepath , ' rb ' ) as f :
if os . path . exists ( tp ) : to = True
file_data = f . read ( )
data_parts . append ( f ' -- { boundary } \r \n ' . encode ( ' utf-8 ' ) )
data_parts . append ( f ' Content-Disposition: form-data; name= " { name } " ; filename= " { filename } " \r \n ' . encode ( ' utf-8 ' ) )
data_parts . append ( f ' Content-Type: { mime_type } \r \n \r \n ' . encode ( ' utf-8 ' ) )
data_parts . append ( file_data )
data_parts . append ( b ' \r \n ' )
except Exception as e :
print ( f " [DEBUG UPLOAD] Erro ao ler arquivo { filepath } : { e } " )
ui . pb . setFormat ( " Limpando Hierarquia e XForm... " ) ; QtWidgets . QApplication . processEvents ( )
add_text_field ( ' tx_hash_login_externo ' , hx_login )
add_text_field ( ' tx_hash_canal ' , hx_canal )
rt . execute ( """
add_text_field ( ' tx_titulo ' , titulo )
(
if ui . rdo_edt . isChecked ( ) and hx_postagem :
local sel = selection as array
add_text_field ( ' tx_hash_postagem ' , hx_postagem )
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 ( )
if not os . path . exists ( tp ) :
try : rt . exportFile ( og , rt . name ( " noPrompt " ) , selectedOnly = True , using = rt . GLTFExporter )
ui . pb . setFormat ( " 📸 Tirando Snapshot Automático (Thumbnail)... " ) ; QtWidgets . QApplication . processEvents ( )
except : rt . execute ( f ' exportFile " { og } " #noPrompt selectedOnly:true ' )
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() ) """ )
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 :
if os . path . exists ( tp ) :
cs = ui . c_chn . currentText ( ) ; ac = " NOVO " if ui . c_pst . currentIndex ( ) == 0 else " ATUALIZAR " ; tit = ui . e_tit . text ( )
add_file_field ( ' thumbnail ' , os . path . basename ( tp ) , tp )
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 ' } "
if os . path . exists ( oz ) :
QtWidgets . QMessageBox . information ( ui , " Cloud API " , msg )
add_file_field ( ' arquivo ' , os . path . basename ( oz ) , oz )
else :
msg = f " Exportado: \n { og } " ;
data_parts . append ( f ' -- { boundary } -- \r \n ' . encode ( ' utf-8 ' ) )
if zo : msg + = f " \n \n 🗜️ ZIP: \n { oz } "
body = b ' ' . join ( data_parts )
if to : msg + = f " \n \n 📸 Thumb: \n { tp } "
QtWidgets . QMessageBox . information ( ui , " Sucesso " , msg )
url = " https://api.vr4life.com/public/savePost "
req = urllib . request . Request ( url , data = body )
req . add_header ( ' Content-Type ' , f ' multipart/form-data; boundary= { boundary } ' )
req . add_header ( ' Content-Length ' , str ( len ( body ) ) )
print ( " \n " + " = " * 50 )
print ( " 📡 [DEBUG API UPLOAD] DADOS ENVIADOS NO FORM-DATA: " )
print ( f " -> tx_hash_login_externo: { hx_login } " )
print ( f " -> tx_hash_canal : { hx_canal } " )
print ( f " -> tx_titulo : { titulo } " )
# Se campo não existe, printa vazio pra entender o contexto
if ui . rdo_edt . isChecked ( ) and hx_postagem :
print ( f " -> tx_hash_postagem : { hx_postagem } " )
print ( f " -> thumbnail (ARQUIVO) : { tp if os . path . exists ( tp ) else ' NAO ENVIADO ' } " )
print ( f " -> arquivo (ARQUIVO) : { oz if os . path . exists ( oz ) else ' NAO ENVIADO ' } " )
print ( f " -------------------------------------------------- " )
print ( f " Disparando pacote de { len ( body ) } bytes para { url } ... " )
print ( " = " * 50 + " \n " )
ui . b_up . setEnabled ( False ) ; ui . b_up . setText ( " Upload em andamento... " )
QtWidgets . QApplication . processEvents ( )
try :
with urllib . request . urlopen ( req , timeout = 120 ) as response :
raw_res = response . read ( ) . decode ( ' utf-8 ' )
print ( f " [DEBUG API UPLOAD] Sucesso HTTP 200: \n { raw_res } " )
QtWidgets . QMessageBox . information ( ui , " Sincronização Cloud Concluída " , f " O projeto foi enviado com sucesso para a plataforma! \n \n Dados Retornados da API: \n { raw_res } " )
except urllib . error . HTTPError as e :
err_body = e . read ( ) . decode ( ' utf-8 ' )
print ( f " [DEBUG API UPLOAD] Erro HTTP { e . code } : \n { err_body } " )
QtWidgets . QMessageBox . critical ( ui , " Sincronização Falhou " , f " O PHP Recusou o Arquivo (HTTP { e . code } ). \n \n Motivo: \n { err_body } " )
except Exception as e :
print ( f " [DEBUG API UPLOAD] Exceção: { e } " )
QtWidgets . QMessageBox . warning ( ui , " Aviso de Rede " , f " Não foi possível completar o envio do arquivo Zip para o servidor. \n \n Detalhes: \n { e } " )
finally :
ui . b_up . setEnabled ( True ) ; ui . b_up . setText ( " ☁️ Enviar para Nuvem " )
ui . pb . setFormat ( " Pronto " ) ; ui . pb . setValue ( 0 )
ui . pb . setFormat ( " Pronto " ) ; ui . pb . setValue ( 0 )