from qgis.core import QgsProject, QgsMapLayer, QgsWkbTypes, Qgis, QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsLayerTreeLayer,  QgsGeometry, QgsLayerTree, QgsVectorLayer, QgsField, QgsPoint, QgsFeature, QgsMeshLayer, edit, QgsProcessingException, QgsTask, QgsApplication, QgsProcessingContext, QgsProcessingFeedback, QgsProcessingAlgRunnerTask
from qgis.PyQt.QtWidgets import QTreeView, QStyledItemDelegate, QColorDialog, QMenu, QDialog, QVBoxLayout, QLabel, QPushButton, QHBoxLayout, QFileDialog, QComboBox, QFrame, QCheckBox, QProgressBar, QListWidget, QScrollBar, QStyle, QGraphicsDropShadowEffect, QDoubleSpinBox, QSpinBox, QRadioButton, QSlider, QGridLayout, QSpacerItem, QSizePolicy, QWidget, QToolTip, QInputDialog, QProgressDialog, QApplication
from qgis.PyQt.QtGui import QStandardItemModel, QStandardItem, QIcon, QPixmap, QPainter, QColor, QPen, QFont, QPalette, QCursor, QDesktopServices, QLinearGradient
from qgis.PyQt.QtCore import Qt, QPoint, QRect, QEvent, QCoreApplication, QSettings, QItemSelectionModel, QSize, QVariant, QModelIndex, QObject, pyqtSignal, QThread, QEventLoop, QUrl, QTimer, pyqtSlot, QRectF, QMetaType
from pyqtgraph.opengl import GLViewWidget, GLLinePlotItem
from pyqtgraph.opengl.shaders import ShaderProgram
from qgis.gui import QgsProjectionSelectionDialog
import xml.etree.ElementTree as ET
import pyqtgraph.opengl as gl
from qgis.utils import iface
from lxml import etree
import pyqtgraph as pg
import numpy as np
import processing
import simplekml
import tempfile
import pathlib
import shutil
import ezdxf
import time
import math
import os
import re

class UiManagerM:
    """
    Gerencia a interface do usuário, interagindo com um QTreeView para listar e gerenciar camadas de malhas no QGIS.
    """
    def __init__(self, iface, dialog):
        """
        Inicializa a instância da classe UiManagerM, responsável por gerenciar a interface do usuário
        que interage com um QTreeView para listar e gerenciar camadas de malhas no QGIS.

        :param iface: Interface do QGIS para interagir com o ambiente.
        :param dialog: Diálogo ou janela que esta classe gerenciará.

        Funções e Ações Desenvolvidas:
        - Configuração inicial das variáveis de instância.
        - Associação do modelo de dados com o QTreeView.
        - Inicialização da configuração do QTreeView.
        - Seleção automática da última camada no QTreeView.
        - Conexão dos sinais do QGIS e da interface do usuário com os métodos correspondentes.
        """
        # Salva as referências para a interface do QGIS e o diálogo fornecidos
        self.iface = iface
        self.dlg = dialog

        # Guards anti-recursão / anti-loop
        self._tree_sync_depth = 0
        self._selection_sync_depth = 0

        # Cria e configura o modelo de dados para o QTreeView
        self.treeViewModel = QStandardItemModel()

        # Aplica estilos CSS para aprimorar a interação visual com os itens do QTreeView
        self.dlg.treeViewListaMalha.setStyleSheet("""
            QTreeView::item:hover:!selected {
                background-color: #def2fc;
            }
            QTreeView::item:selected {
            }""")

        # Atualiza a visualização da lista de camadas no QTreeView
        self.atualizar_treeView_lista_malha()

        self.dlg.treeViewListaMalha.setModel(self.treeViewModel)

        # Inicia a conexão de sinais para tratar a mudança de nome das camadas no projeto
        self.connect_name_changed_signals()

        # Conecta os sinais do QGIS e da interface do usuário para sincronizar ações e eventos
        self.connect_signals()

        # Chama atualização dos botões após toda modificação
        self.atualizar_estado_botoes()

        # Adiciona o filtro de eventos ao treeView
        self.tree_view_event_filter = TreeViewEventFilter(self)
        self.dlg.treeViewListaMalha.viewport().installEventFilter(self.tree_view_event_filter)

    def connect_signals(self):
        """
        Conecta os sinais do QGIS e do QTreeView para sincronizar a interface com o estado atual do projeto.
        Este método garante que mudanças no ambiente do QGIS se reflitam na interface do usuário e que ações na
        interface desencadeiem reações apropriadas no QGIS.

        Funções e Ações Desenvolvidas:
        - Conexão com sinais de adição e remoção de camadas para atualizar a visualização da árvore.
        - Sincronização do modelo do QTreeView com mudanças de seleção e propriedades das camadas no QGIS.
        - Tratamento da mudança de nome das camadas para manter consistência entre a interface e o estado interno.
        """
        # Conecta sinais do QGIS para lidar com a adição e remoção de camadas no projeto
        QgsProject.instance().layersAdded.connect(self.layers_added)

        # Conecta o evento de mudança em um item do QTreeView para atualizar a visibilidade da camada no QGIS
        self.treeViewModel.itemChanged.connect(self.on_item_changed)

        # Define e aplica um delegado personalizado para customização da exibição de itens no QTreeView
        self.dlg.treeViewListaMalha.setItemDelegate(CustomDelegate(self.dlg.treeViewListaMalha))

        # Sincroniza o estado das camadas no QGIS com o checkbox do QTreeView sempre que as camadas do mapa mudam
        self.iface.mapCanvas().layersChanged.connect(self.sync_from_qgis_to_treeview)

        # Conecta mudanças na seleção do QTreeView para atualizar a camada ativa no QGIS
        self.dlg.treeViewListaMalha.selectionModel().selectionChanged.connect(self.on_treeview_selection_changed)

        # Sincroniza a seleção no QGIS com a seleção no QTreeView quando a camada ativa no QGIS muda
        self.iface.currentLayerChanged.connect(self.on_current_layer_changed)

        # Configura a política de menu de contexto para permitir menus personalizados em cliques com o botão direito
        self.dlg.treeViewListaMalha.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.dlg.treeViewListaMalha.customContextMenuRequested.connect(self.open_context_menu)

        # Conectar o botão à função 3D
        self.dlg.pushButtonMalha3D.clicked.connect(self.mostrar_dialogo_exporta_malha_3d)

        # Conectar o botão à função KML
        self.dlg.pushButtonMalhaKML.clicked.connect(self.exportar_malha_kml)

        # Conectando o botão pushButton3DMalha
        self.dlg.pushButton3DMalha.clicked.connect(self.abrir_visualizador_malha_3d)

        # Conectando o botão pushButtonFecharM à função que fecha o diálogo
        self.dlg.pushButtonFecharM.clicked.connect(self.close_dialog)

        # seleção via mouse (funciona mesmo com 1 Malha)
        self.dlg.treeViewListaMalha.clicked.connect(lambda _: self.on_treeview_selection_changed(None, None))

        # Ação de controle ao remover Camadas de Rasters
        QgsProject.instance().layersRemoved.connect(self.on_layers_removed)

        # Conecta o botão para Zoom
        self.dlg.pushButtonVisualizarM.clicked.connect(self.visualizar_raster_selecionada)

        # Conectar o botão de renomear à função de renomear a camada
        self.dlg.pushButtonRenomeM.clicked.connect(self.renomear_camada_selecionada)

        # Conectar o botão de deletar à função de remover a camada
        self.dlg.pushButtonDelM.clicked.connect(self.remover_camada_selecionada)

        # Conectar o botão de salvar à função de salvar a camada
        self.dlg.pushButtonSalvarMalha.clicked.connect(self.salvar_malha_selecionada)

        # Conectar o botão de clocar à função de Clonar a camada
        self.dlg.pushButtonClonaMalha.clicked.connect(self.clonar_malha_selecionada)

        # Conectar o botão de Reprojetar à função de Reprojetar a camada
        self.dlg.pushButtonReprojetarM.clicked.connect(self.reprojetar_malha_selecionada)

        # Abre para adicionar camada de Malhas
        self.dlg.pushButtonAbrirM.clicked.connect(self.abrir_malha_arquivo)

    def abrir_malha_arquivo(self):
        """
        Abre um diálogo para selecionar um arquivo de malha compatível com MDAL
        (.2dm, .mesh, .nc, .ply, .slf), carrega como QgsMeshLayer e adiciona ao projeto QGIS.
        """
        filtros = "Arquivos de Malha (*.2dm *.mesh *.nc *.slf);;Todos os Arquivos (*)"

        file_path, _ = QFileDialog.getOpenFileName(self.dlg, "Abrir Arquivo de Malha", "", filtros)

        if not file_path:
            return

        nome_camada = os.path.splitext(os.path.basename(file_path))[0]
        layer = QgsMeshLayer(file_path, nome_camada, "mdal")

        if not layer.isValid():
            self.mostrar_mensagem("Falha ao carregar a malha.\nVerifique se o arquivo é compatível com o MDAL.", "Erro")
            return

        QgsProject.instance().addMapLayer(layer)
        self.mostrar_mensagem(f"Camada de malha '{nome_camada}' adicionada com sucesso!", "Sucesso")

        self.atualizar_treeView_lista_malha()

    def salvar_malha_selecionada(self):
        """
        Salva a camada de malha selecionada no treeViewListaMalha no formato .mesh.

        Passos:
        1. Obtém a camada de malha selecionada no treeView.
        2. Pede ao usuário o caminho para salvar o arquivo .mesh.
        3. Salva o arquivo usando o provedor de dados da malha.
        4. Mostra mensagem de sucesso ou erro.
        """
        # Obtém a seleção no treeView
        indexes = self.dlg.treeViewListaMalha.selectedIndexes()
        if not indexes:
            self.mostrar_mensagem("Nenhuma camada de malha selecionada.", "Erro")
            return

        selected_index = indexes[0]
        # Recupera o ID da camada usando o modelo do treeView
        layer_id = self.treeViewModel.itemFromIndex(selected_index).data(Qt.ItemDataRole.UserRole)
        layer = QgsProject.instance().mapLayer(layer_id)
        if not layer or layer.type() != QgsMapLayer.LayerType.MeshLayer:
            self.mostrar_mensagem("Camada selecionada não é uma malha válida.", "Erro")
            return

        # Sugere um nome baseado na camada
        nome_sugerido = layer.name() + ".mesh"
        path_saida = self.escolher_local_para_salvar(nome_sugerido, "Arquivo de Malha (*.mesh)")
        if not path_saida:
            return

        # Tenta salvar a malha usando o provedor de dados
        # O método saveMeshLayer depende do provedor (e tipo de malha)
        provedor = layer.dataProvider()
        # Verifica se o provedor suporta salvar/exportar
        try:
            # QGIS >= 3.14: meshLayer.saveMesh(path, driverName)
            # Driver name pode ser 'MDAL Mesh'
            if hasattr(layer, "saveMesh"):
                # Salva utilizando o método da camada (QGIS >= 3.14)
                ok = layer.saveMesh(path_saida, "MDAL Mesh")
            else:
                # Alternativamente, pode tentar copiar o arquivo fonte (se existir)
                ok = False
                src_path = provedor.dataSourceUri()
                if os.path.isfile(src_path):
                    import shutil
                    shutil.copyfile(src_path, path_saida)
                    ok = True
            if ok:
                self.mostrar_mensagem("Malha salva com sucesso!", "Sucesso",
                                      caminho_arquivo=path_saida,
                                      caminho_pasta=os.path.dirname(path_saida))
            else:
                self.mostrar_mensagem("Erro ao salvar malha. Verifique se o provedor suporta exportação.",
                                      "Erro")
        except Exception as e:
            import traceback
            print("[EXCEPTION] salvar_malha_selecionada:", e)
            print(traceback.format_exc())
            self.mostrar_mensagem(f"Erro ao salvar malha: {e}", "Erro")

    def remover_camada_selecionada(self):
        """
        Remove a camada de malha (mesh) selecionada no treeViewListaMalha sem perguntar nada ao usuário.
        """
        # Obtém os índices selecionados no treeView de malha
        selected_indexes = self.dlg.treeViewListaMalha.selectedIndexes()
        if selected_indexes:
            selected_index = selected_indexes[0]
            # Usa o papel UserRole para pegar o ID real da camada
            layer_id = selected_index.model().itemFromIndex(selected_index).data(Qt.ItemDataRole.UserRole)
            layer_to_remove = QgsProject.instance().mapLayer(layer_id)
            if layer_to_remove:
                # Remove a camada de malha diretamente do projeto QGIS
                QgsProject.instance().removeMapLayer(layer_id)
                self.atualizar_treeView_lista_malha()
                self.iface.mapCanvas().refresh()

    def renomear_camada_selecionada(self):
        """
        Permite ao usuário renomear uma camada de Malha selecionado no treeViewListaMalha,
        garantindo unicidade do nome no projeto.

        Fluxo:
        - Obtém o índice selecionado.
        - Recupera a camada associada ao item (usando Qt.UserRole).
        - Exibe um diálogo para o usuário digitar o novo nome.
        - Garante que o nome será único (ajusta se necessário).
        - Atualiza o nome da camada no QGIS e o texto do item no treeView.
        """
        # Obtém os índices atualmente selecionados no treeView de malha
        selected_indexes = self.dlg.treeViewListaMalha.selectedIndexes()
        if selected_indexes:
            # Pega o primeiro índice selecionado
            selected_index = selected_indexes[0]
            # Recupera o ID da camada usando o papel UserRole (garante que é o ID real da camada QGIS)
            layer_id = selected_index.model().itemFromIndex(selected_index).data(Qt.ItemDataRole.UserRole)
            # Busca a camada correspondente no projeto QGIS
            selected_layer = QgsProject.instance().mapLayer(layer_id)
            if selected_layer:
                # Exibe um diálogo para o usuário digitar o novo nome (pré-preenchido com o nome atual)
                novo_nome, ok = QInputDialog.getText(
                    self.dlg,
                    "Renomear Camada",
                    "Digite o novo nome da camada:",
                    text=selected_layer.name())
                # Se o usuário confirmou e digitou algo
                if ok and novo_nome:
                    # Gera um nome único para evitar duplicidade no projeto
                    novo_nome = self.gerar_nome_unico(novo_nome, selected_layer.id())
                    # Renomeia a camada no QGIS (isso pode disparar sinais que atualizam a interface automaticamente)
                    selected_layer.setName(novo_nome)
                    # Em vez de tentar alterar diretamente o item do modelo (risco de crash), reconstrói o treeView do zero
                    self.atualizar_treeView_lista_malha()

    def gerar_nome_unico(self, base_nome, current_layer_id):
        """
        Gera um nome único para uma camada de malha (mesh) dentro do projeto QGIS,
        garantindo que não haja conflitos de nome entre as camadas de malha.

        Parâmetros:
        - base_nome (str): Nome base proposto para a camada.
        - current_layer_id (str): ID da camada que está sendo renomeada, usado para ignorar ela mesma.

        Funcionalidades:
        - Cria um dicionário dos nomes de todas as camadas mesh existentes no projeto QGIS, exceto a camada renomeada.
        - Se o nome base não existir, retorna ele mesmo.
        - Se já existir, incrementa um sufixo numérico (_1, _2, ...) até encontrar um nome único.
        - Retorna o novo nome gerado.
        """
        # Só considera camadas de malha (mesh)
        existing_names = {layer.name(): layer.id()
            for layer in QgsProject.instance().mapLayers().values()
            if (layer.id() != current_layer_id and layer.type() == QgsMapLayer.LayerType.MeshLayer)}
        if base_nome not in existing_names:
            return base_nome
        else:
            i = 1
            novo_nome = f"{base_nome}_{i}"
            while novo_nome in existing_names:
                i += 1
                novo_nome = f"{base_nome}_{i}"
            return novo_nome

    def visualizar_raster_selecionada(self):
        """
        Aproxima a visualização do mapa para a camada de malha selecionada no treeView.

        - Obtém a camada de malha atualmente selecionada.
        - Centraliza e ajusta a extensão do mapa para mostrar toda a camada.
        - Se não houver feições ou seleção, não faz nada.
        """
        index = self.dlg.treeViewListaMalha.currentIndex()  # Obtém o índice selecionado
        if not index.isValid():
            return  # Não há seleção

        # Obtém o ID da camada do UserRole (melhor do que usar .data() sem argumentos)
        layer_id = index.model().itemFromIndex(index).data(Qt.ItemDataRole.UserRole)
        layer = QgsProject.instance().mapLayer(layer_id)
        if not layer or layer.extent().isEmpty():
            return  # Camada não encontrada ou sem feições

        # Garante que a camada esteja ativa (opcional, mas recomendado)
        self.iface.setActiveLayer(layer)
        # Ajusta o zoom para a extensão da camada ativa
        self.iface.zoomToActiveLayer()

    def atualizar_estado_botoes(self):
        """
        Atualiza o estado (habilitado/desabilitado) dos botões da interface gráfica conforme a presença de camadas no treeView de Rasters.

        Detalhamento do Processo:
        1. Verifica se o modelo associado ao treeViewListaMalha está vazio, ou seja, sem nenhuma camada listada.
        2. Se o modelo estiver vazio, todos os botões que dependem de camada devem ser desabilitados para evitar operações inválidas.
        3. Caso contrário, os botões são habilitados, permitindo ao usuário interagir normalmente com as funções associadas a uma camada de Malha.
        4. Os botões afetados incluem:
            - Deletar camada (`pushButtonDelM`)
            - Renomear camada (`pushButtonRenomeM`)
            - Tornar camada permanente (`pushButtonSalvarMalha`)
            - Salvar em múltiplos formatos (`pushButtonSalvaMultiplosM`)
            - Exportar para DXF/DAE/STL/OBJ (`pushButtonMalha3D`)
            - Exportar para KML (`pushButtonRasterKML`)
            - Visualizar pontos associados (`pushButtonVisualizarM`)
            - Vizzualizar 3D (`pushButton3DMalha`)
            - Reprojetar camada (`pushButtonReprojetarR`)
        5. Garante uma experiência segura e intuitiva, evitando erros por ações em contexto inválido.
        """
        # Verifica se o modelo do treeView está vazio
        modelo_vazio = self.dlg.treeViewListaMalha.model().rowCount() == 0

        # Atualiza o estado dos botões baseado na presença ou ausência de itens no modelo
        self.dlg.pushButtonDelM.setEnabled(not modelo_vazio)
        self.dlg.pushButtonRenomeM.setEnabled(not modelo_vazio)
        self.dlg.pushButtonSalvarMalha.setEnabled(not modelo_vazio)
        # self.dlg.pushButtonSalvaMultiplosM.setEnabled(not modelo_vazio)
        self.dlg.pushButtonMalha3D.setEnabled(not modelo_vazio)
        self.dlg.pushButtonMalhaKML.setEnabled(not modelo_vazio)
        self.dlg.pushButtonVisualizarM.setEnabled(not modelo_vazio)
        self.dlg.pushButtonClonaMalha.setEnabled(not modelo_vazio)
        self.dlg.pushButtonReprojetarM.setEnabled(not modelo_vazio)
        self.dlg.pushButton3DMalha.setEnabled(not modelo_vazio)

    def clonar_malha_selecionada(self):
        """
        Clona a camada de malha selecionada no treeViewListaMalha, criando uma camada idêntica
        como camada temporária no QGIS. Não funciona para malhas de provedores online.
        """
        idxs = self.dlg.treeViewListaMalha.selectedIndexes()
        if not idxs:
            self.mostrar_mensagem("Nenhuma camada selecionada.", "Aviso")
            return

        layer_id = self.treeViewModel.itemFromIndex(idxs[0]).data(Qt.ItemDataRole.UserRole)
        layer = QgsProject.instance().mapLayer(layer_id)
        if not layer or layer.type() != QgsMapLayer.LayerType.MeshLayer:
            self.mostrar_mensagem("Selecione uma camada de malha válida.", "Erro")
            return

        # Verifica se é camada de provedor (serviço online)
        if layer.providerType().lower() not in ("mdal",):  # MDAL = arquivo local de malha
            self.mostrar_mensagem(
                "Não é possível clonar malhas de provedores online.\n"
                "Somente malhas locais (arquivos no disco) podem ser clonadas.", "Aviso")
            return

        # Gera nome e caminho temporário do clone
        import tempfile, shutil, os
        clone_name = f"{layer.name()}_Clone"
        src_path = layer.dataProvider().dataSourceUri()
        ext = os.path.splitext(src_path)[-1]
        tmp_dir = tempfile.mkdtemp()
        clone_path = os.path.join(tmp_dir, f"{clone_name}{ext}")

        try:
            shutil.copy(src_path, clone_path)
        except Exception as e:
            self.mostrar_mensagem(f"Erro ao clonar arquivo da malha: {e}", "Erro")
            shutil.rmtree(tmp_dir)
            return

        # Cria nova camada mesh no QGIS usando o arquivo clonado
        from qgis.core import QgsMeshLayer
        clone_layer = QgsMeshLayer(clone_path, clone_name, "mdal")

        if clone_layer.isValid():
            # Tenta copiar estilo da camada original (só funciona para simbologia do QGIS)
            try:
                clone_layer.importNamedStyle(layer.styleManager().style(layer.styleManager().currentStyle()))
            except Exception:
                pass
            clone_layer.setCrs(layer.crs())
            QgsProject.instance().addMapLayer(clone_layer)
            self.mostrar_mensagem("Clone de malha criado com sucesso!", "Sucesso")
        else:
            self.mostrar_mensagem("Erro ao criar o clone de malha!", "Erro")
            shutil.rmtree(tmp_dir)  # Limpa temp

    def reprojetar_malha_selecionada(self):
        """
        Permite ao usuário atribuir SRC se a malha não tiver,
        ou reprojetar a malha selecionada usando o algoritmo 'native:reprojectmesh'.
        """
        idxs = self.dlg.treeViewListaMalha.selectedIndexes()
        if not idxs:
            self.mostrar_mensagem("Nenhuma camada de malha selecionada.", "Aviso")
            return

        layer_id = self.treeViewModel.itemFromIndex(idxs[0]).data(Qt.ItemDataRole.UserRole)
        layer = QgsProject.instance().mapLayer(layer_id)
        if not layer or layer.type() != QgsMapLayer.LayerType.MeshLayer:
            self.mostrar_mensagem("Selecione uma camada de malha válida.", "Erro")
            return

        if not layer.crs().isValid():
            # Se não tem SRC, abre diálogo para atribuir (não reprojeta, só define SRC)
            crs = self.selecionar_crs_dialogo("Definir SRC para a malha")
            if not crs:
                self.mostrar_mensagem("SRC não definido!", "Erro")
                return
            layer.setCrs(crs)
            self.mostrar_mensagem("SRC definido na malha.", "Informação")
            return

        # Tem SRC, pede SRC de destino
        crs_destino = self.selecionar_crs_dialogo("Selecionar SRC de destino")
        if not crs_destino or crs_destino == layer.crs():
            self.mostrar_mensagem("SRC de destino não selecionado ou igual ao atual.", "Aviso")
            return

        # Executa a reprojeção real (gera nova camada de malha)
        params = {
            'INPUT': layer,
            'TARGET_CRS': crs_destino,
            'OUTPUT': 'TEMPORARY_OUTPUT'}
        try:
            result = processing.run("native:reprojectmesh", params)
            output_mesh = result['OUTPUT']
            clone_name = f"{layer.name()}_reprojetada"
            clone_layer = QgsMeshLayer(output_mesh, clone_name, "mdal")
            if clone_layer.isValid():
                clone_layer.setCrs(crs_destino)
                QgsProject.instance().addMapLayer(clone_layer)
                self.mostrar_mensagem("Malha reprojetada com sucesso!", "Sucesso")
            else:
                self.mostrar_mensagem("Erro ao carregar malha reprojetada!", "Erro")
        except Exception as e:
            self.mostrar_mensagem(f"Erro ao reprojetar a malha: {e}", "Erro")

    def selecionar_crs_dialogo(self, titulo="Selecionar SRC"):
        """
        Abre o diálogo padrão do QGIS para seleção de SRC e retorna um QgsCoordinateReferenceSystem.
        """
        dlg = QgsProjectionSelectionDialog()
        dlg.setWindowTitle(titulo)
        if dlg.exec():
            epsg = dlg.crs().authid()
            return QgsCoordinateReferenceSystem(epsg)
        return None

    def configurar_tooltip(self, index):
        """
        Exibe um tooltip com informações detalhadas sobre a camada de malha (mesh) selecionada no treeView.

        Detalhamento do Processo:
        1. Obtém o item do modelo de dados correspondente ao índice informado.
        2. Recupera o ID da camada mesh associada ao item e acessa a camada no projeto QGIS.
        3. Valida se a camada encontrada é realmente uma MeshLayer.
        4. Extrai informações relevantes da malha, incluindo:
            - Tipo de camada ("Mesh")
            - SRC (sistema de referência de coordenadas)
            - Número de nós (vertices)
            - Número de faces (elementos)
        5. Formata todas essas informações em um texto amigável.
        6. Exibe o tooltip na posição atual do cursor do mouse.
        """
        item = index.model().itemFromIndex(index)
        layer_id = item.data(Qt.ItemDataRole.UserRole)
        layer = QgsProject.instance().mapLayer(layer_id)

        if layer and layer.type() == QgsMapLayer.LayerType.MeshLayer:
            # Tipo de camada (Mesh)
            tipo_malha = self.obter_tipo_de_malha(layer)

            # SRC
            crs = layer.crs().description() if layer.crs().isValid() else "Sem Georreferenciamento"

            # Fonte do dado (arquivo/path)
            source = layer.source()

            # Obtém o provider para informações de geometria
            prov = layer.dataProvider()

            # Número de nós (vértices)
            num_vertices = prov.vertexCount() if hasattr(prov, "vertexCount") else "N/D"

            # Número de faces (elementos)
            num_faces = prov.faceCount() if hasattr(prov, "faceCount") else "N/D"

            # Monta o texto do tooltip
            tooltip_text = (
                f"SRC: {crs}\n"
                f"Arquivo: {source}\n"
                f"Vértices: {num_vertices}\n"
                f"Faces: {num_faces}")

            # Exibe o tooltip
            QToolTip.showText(QCursor.pos(), tooltip_text)

    def obter_tipo_de_malha(self, layer):
        """
        Retorna uma string descritiva do tipo de camada mesh.

        Para camadas mesh padrão do QGIS, retorna 'Mesh'.
        """
        if layer.type() == QgsMapLayer.LayerType.MeshLayer:
            return "Mesh"
        return "Desconhecido"

    def close_dialog(self):
        """
        Fecha o diálogo associado a este UiManagerM:
        """
        self.dlg.close()

    def atualizar_treeView_lista_malha(self):
        """
        Atualiza o TreeView que lista as camadas de malhas no QGIS.
        """

        tv = self.dlg.treeViewListaMalha

        # 0) GARANTE que o TreeView tem model (evita model() == None)
        if tv.model() is not self.treeViewModel:
            tv.setModel(self.treeViewModel)

        # 1) Guarda o layer_id selecionado ANTES de recriar o modelo
        current_layer_id = None
        current_index = tv.currentIndex()
        if current_index.isValid():
            current_layer_id = current_index.data(Qt.ItemDataRole.UserRole)

        # 2) Reconstrói o modelo
        self.treeViewModel.clear()

        headerItem = QStandardItem("Lista de Camadas de Malhas")
        headerItem.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
        self.treeViewModel.setHorizontalHeaderItem(0, headerItem)

        root = QgsProject.instance().layerTreeRoot()

        for layer in QgsProject.instance().mapLayers().values():
            if layer.type() == QgsMapLayer.LayerType.MeshLayer:
                item = QStandardItem(layer.name())
                item.setCheckable(True)
                item.setEditable(False)
                item.setData(layer.id(), Qt.ItemDataRole.UserRole)

                layerNode = root.findLayer(layer.id())
                is_visible = bool(layerNode and layerNode.isVisible())
                item.setCheckState(Qt.CheckState.Checked if is_visible else Qt.CheckState.Unchecked)

                self.adjust_item_font(item, layer)
                self.treeViewModel.appendRow(item)

        # 3) Seleção/scroll (AGORA fora do loop)
        row_count = self.treeViewModel.rowCount()
        if row_count == 0:
            return

        target_row = 0
        if current_layer_id:
            for r in range(row_count):
                it = self.treeViewModel.item(r, 0)
                if it and it.data(Qt.ItemDataRole.UserRole) == current_layer_id:
                    target_row = r
                    break

        target_index = self.treeViewModel.index(target_row, 0)
        tv.setCurrentIndex(target_index)
        tv.scrollTo(target_index)

    def on_current_layer_changed(self, layer):
        """
        Sincroniza a seleção do treeView com a camada ativa no QGIS.
        """
        if getattr(self, "_selection_sync_depth", 0) > 0:
            return

        model = self.dlg.treeViewListaMalha.model()
        if model is None or model.rowCount() == 0:
            return

        if not layer or layer.type() != QgsMapLayer.LayerType.MeshLayer:
            return

        layer_id = layer.id()

        for row in range(model.rowCount()):
            item = model.item(row, 0)
            if item and item.data(Qt.ItemDataRole.UserRole) == layer_id:
                index = model.indexFromItem(item)
                current = self.dlg.treeViewListaMalha.currentIndex()

                if current != index:
                    self._selection_sync_depth += 1
                    try:
                        self.dlg.treeViewListaMalha.setCurrentIndex(index)
                        self.dlg.treeViewListaMalha.scrollTo(index)
                    finally:
                        self._selection_sync_depth = max(0, self._selection_sync_depth - 1)
                break

    def on_item_changed(self, item):
        """
        Atualiza a visibilidade da camada correspondente no painel de camadas do QGIS.
        """
        if getattr(self, "_tree_sync_depth", 0) > 0:
            return

        layer_id = item.data(Qt.ItemDataRole.UserRole)
        if not layer_id:
            return

        layer = QgsProject.instance().mapLayer(layer_id)
        if not layer:
            return

        root = QgsProject.instance().layerTreeRoot()
        node = root.findLayer(layer.id())
        if not node:
            return

        desired = (item.checkState() == Qt.CheckState.Checked)

        try:
            current = bool(node.itemVisibilityChecked())
        except Exception:
            current = bool(node.isVisible())

        if current != desired:
            node.setItemVisibilityChecked(desired)

    def on_layers_removed(self, removed_ids):
        """
        Remove do treeView apenas as linhas correspondentes às malhas removidas do projeto.
        """
        if self.treeViewModel.rowCount() == 0:
            return

        self._tree_sync_depth += 1
        self._selection_sync_depth += 1
        try:
            last_removed_row = 0

            for rid in removed_ids:
                for row in range(self.treeViewModel.rowCount()):
                    item = self.treeViewModel.item(row)
                    if item and item.data(Qt.ItemDataRole.UserRole) == rid:
                        last_removed_row = row
                        self.treeViewModel.removeRow(row)
                        break

            model = self.dlg.treeViewListaMalha.model()
            if model is not None and model.rowCount() > 0:
                current_index = self.dlg.treeViewListaMalha.currentIndex()
                if not current_index.isValid():
                    target_row = min(last_removed_row, model.rowCount() - 1)
                    idx = model.index(target_row, 0)
                    self.dlg.treeViewListaMalha.setCurrentIndex(idx)
                    self.dlg.treeViewListaMalha.scrollTo(idx)
        finally:
            self._selection_sync_depth = max(0, self._selection_sync_depth - 1)
            self._tree_sync_depth = max(0, self._tree_sync_depth - 1)

        self.atualizar_estado_botoes()

    def on_treeview_selection_changed(self, selected, deselected):
        """
        Descrição:
        Função chamada sempre que a seleção no QTreeView de malhas é alterada.
        Ela obtém o item selecionado e, caso corresponda a uma camada de malha,
        torna essa camada ativa no QGIS.

        Parâmetros:
        - selected (QItemSelection): Itens que passaram a ser selecionados.
        - deselected (QItemSelection): Itens que deixaram de ser selecionados.

        Passos:
        1. Obtém os índices dos itens atualmente selecionados no treeView.
        2. Se não houver seleção, encerra sem fazer nada.
        3. Recupera o ID da camada armazenado no primeiro item selecionado.
        4. Busca o objeto QgsMapLayer a partir desse ID no projeto.
        5. Se a camada existir e for válida, define-a como camada ativa na interface.
        """
        # 1. Obtém os índices dos itens selecionados no QTreeView de malhas
        indexes = self.dlg.treeViewListaMalha.selectionModel().selectedIndexes()
        # 2. Verifica se não há nenhum índice selecionado; se não houver, sai da função
        if not indexes:
            return

        # 3. Recupera a ID da camada armazenada em Qt.UserRole do primeiro índice
        layer_id = self.treeViewModel.itemFromIndex(indexes[0]).data(Qt.ItemDataRole.UserRole)
        # 4. Busca a camada no projeto QGIS usando a ID recuperada
        layer = QgsProject.instance().mapLayer(layer_id)
        # 5. Se a camada existir, define-a como ativa na interface do QGIS
        if layer:
            self.iface.setActiveLayer(layer)

    def on_layer_was_renamed(self, layerId, newName):
        """
        Descrição:
        Função chamada sempre que uma camada é renomeada no projeto QGIS.
        Atualiza o item correspondente no treeView de malhas para exibir o novo nome.

        Parâmetros:
        - layerId (str): ID única da camada que foi renomeada.
        - newName (str): Novo nome atribuído à camada.

        Passos:
        1. Percorrer todas as linhas do modelo do treeView.
        2. Para cada item, verificar se o dado em Qt.UserRole é igual a layerId.
        3. Se encontrar correspondência, atualizar o texto do item com newName.
        4. Interromper o loop após atualizar o nome.
        """
        # 1. Percorre todas as linhas do modelo do treeView de malhas
        for i in range(self.treeViewModel.rowCount()):
            # 1.1. Obtém o item da linha i, coluna 0
            item = self.treeViewModel.item(i)
            # 2. Verifica se o UserRole (ID da camada) coincide com o layerId passado
            if item.data(Qt.ItemDataRole.UserRole) == layerId:
                # 3. Atualiza o texto do item para refletir o novo nome da camada
                item.setText(newName)
                # 4. Sai do loop, pois já atualizamos o item correto
                break

    def sync_from_qgis_to_treeview(self):
        """
        Descrição:
        Função chamada sempre que há mudança nas camadas do canvas ou no projeto QGIS.
        Sincroniza o estado de visibilidade das camadas de malha exibidas no QTreeView
        com o estado real de visibilidade no painel de camadas.

        Parâmetros:
        - nenhum

        Passos:
        1. Obter o nó raiz da árvore de camadas do projeto.
        2. Percorrer todas as linhas do modelo do treeView de malhas.
        3. Para cada item, recuperar o ID da camada armazenado em Qt.UserRole.
        4. Localizar o nó da camada correspondente na árvore de camadas.
        5. Se o nó existir, atualizar o estado de check do item conforme a visibilidade.
        """
        # 1. Obter o nó raiz da árvore de camadas do projeto QGIS
        root = QgsProject.instance().layerTreeRoot()
        # 2. Percorrer todas as linhas do modelo do treeViewListaMalha
        for i in range(self.treeViewModel.rowCount()):
            # 2.1. Obter o item da linha i (coluna 0)
            item = self.treeViewModel.item(i)
            # 2.2. Recuperar o ID da camada armazenado em Qt.UserRole
            layer_id = item.data(Qt.ItemDataRole.UserRole)
            # 2.3. Localizar o nó da camada correspondente pelo ID
            layerNode = root.findLayer(layer_id)
            # 2.4. Se o nó existir, ajustar o estado de check conforme visibilidade
            if layerNode:
                # 2.4.1. Definir Checked se visível, Unchecked caso contrário
                item.setCheckState(Qt.CheckState.Checked if layerNode.isVisible() else Qt.CheckState.Unchecked)

    def adjust_item_font(self, item, layer):
        """
        Esta função ajusta a fonte do item no QTreeView com base no tipo de camada.
        Se a camada for temporária (dados em memória), ajusta a fonte para itálico.
        Se a camada for permanente, ajusta a fonte para negrito.

        Detalhes:
        - Cria um objeto QFont para ajustar a fonte do item.
        - Verifica se a camada é temporária usando o método isTemporary().
        - Se a camada for temporária, ajusta a fonte para itálico.
        - Se a camada for permanente, ajusta a fonte para negrito.
        - Aplica a fonte ajustada ao item no QTreeView.
        - Retorna o item com a fonte ajustada para uso posterior, se necessário.
        """
        # Cria um objeto QFont para ajustar a fonte do item
        fonte_item = QFont()

        # Verifica se a camada é temporária (dados em memória) e ajusta a fonte para itálico
        if layer.isTemporary():
            fonte_item.setItalic(True)
        # Se não for temporária, ajusta a fonte para negrito, indicando uma camada permanente
        else:
            fonte_item.setBold(True)

        # Aplica a fonte ajustada ao item no QTreeView
        item.setFont(fonte_item)

        # Retorna o item com a fonte ajustada para uso posterior se necessário
        return item

    def connect_name_changed_signals(self):
        """
        Conecta o sinal nameChanged de todas as camadas reais do projeto.
        Ignora grupos e nós sem camada associada.
        Evita conexões duplicadas.
        """
        root = QgsProject.instance().layerTreeRoot()

        for layerNode in root.findLayers():
            if isinstance(layerNode, QgsLayerTreeLayer):
                layer = layerNode.layer()
                if layer is not None:
                    try:
                        # desconecta primeiro para evitar duplicidade
                        layer.nameChanged.disconnect(self.on_layer_name_changed)
                    except TypeError:
                        # se não estava conectado, ignora o erro
                        pass

                    # conecta de forma segura
                    layer.nameChanged.connect(self.on_layer_name_changed)

    def layers_added(self, layers):
        """
        Responde ao evento de adição de camadas no projeto QGIS, atualizando a lista de camadas no QTreeView
        e conectando sinais de mudança de nome para camadas de malhas recém-adicionadas.

        Este método verifica cada camada adicionada para determinar se é uma camada de vetor de malhas.
        Se for, ele atualiza a lista de camadas no QTreeView e conecta o sinal de mudança de nome à função
        de callback apropriada.

        :param layers: Lista de camadas recém-adicionadas ao projeto.

        Funções e Ações Desenvolvidas:
        - Verificação do tipo e da geometria das camadas adicionadas.
        - Atualização da visualização da lista de camadas no QTreeView para incluir novas camadas de malhas.
        - Conexão do sinal de mudança de nome da camada ao método de tratamento correspondente.
        """
        # Itera por todas as camadas adicionadas
        for layer in layers:
            # Verifica se a camada é do tipo vetor e se sua geometria é de malha
            if layer.type() == QgsMapLayer.LayerType.MeshLayer:
                # Atualiza a lista de camadas no QTreeView
                self.atualizar_treeView_lista_malha()
                # Conecta o sinal de mudança de nome da nova camada ao método on_layer_name_changed
                layer.nameChanged.connect(self.on_layer_name_changed)
                # Interrompe o loop após adicionar o sinal à primeira camada de malha encontrada
                break

        # Chama atualização dos botões após toda modificação
        self.atualizar_estado_botoes()

    def on_layer_name_changed(self):
        """
        Responde ao evento de mudança de nome de qualquer camada no projeto QGIS. 
        Este método é chamado automaticamente quando o nome de uma camada é alterado,
        e sua função é garantir que a lista de camadas no QTreeView seja atualizada para refletir
        essa mudança.

        Funções e Ações Desenvolvidas:
        - Atualização da lista de camadas no QTreeView para assegurar que os nomes das camadas estejam corretos.
        """
        # Atualiza a lista de camadas no QTreeView para refletir a mudança de nome
        self.atualizar_treeView_lista_malha()

    def open_context_menu(self, position):
        """
        Esta função abre um menu de contexto ao clicar com o botão direito na árvore de visualização (QTreeView).
        Se um item for selecionado, cria e exibe um menu de contexto com a opção de abrir as propriedades da camada.
        
        Detalhes:
        - Obtém os índices dos itens selecionados no QTreeView.
        - Verifica se há algum item selecionado.
        - Se houver um item selecionado:
            - Cria um novo menu de contexto.
            - Adiciona a opção "Abrir Propriedades da Camada" ao menu de contexto.
            - Exibe o menu de contexto na posição do cursor.
            - Executa a ação correspondente se a opção "Abrir Propriedades da Camada" for selecionada.
        """
        # Obtém os índices dos itens selecionados na árvore de visualização
        indexes = self.dlg.treeViewListaMalha.selectedIndexes()
        
        # Verifica se algum item foi selecionado
        if indexes:
            # Cria um novo menu de contexto
            menu = QMenu()
            
            # Adiciona uma ação ao menu de contexto
            layer_properties_action = menu.addAction("Abrir Propriedades da Camada")
            
            # Exibe o menu de contexto na posição do cursor e obtém a ação selecionada pelo usuário
            action = menu.exec(self.dlg.treeViewListaMalha.viewport().mapToGlobal(position))
            
            # Verifica se a ação selecionada foi "Abrir Propriedades da Camada"
            if action == layer_properties_action:
                # Abre as propriedades da camada para o item selecionado
                self.abrir_layer_properties(indexes[0])

    def abrir_layer_properties(self, index):
        """
        Abre a janela de propriedades da camada selecionada no QTreeView. Este método é chamado quando o usuário deseja
        ver ou editar as propriedades de uma camada específica, como símbolos, campos e outras configurações.

        Funções e Ações Desenvolvidas:
        - Obtenção do ID da camada a partir do item selecionado no QTreeView.
        - Recuperação da camada correspondente no projeto QGIS.
        - Exibição da janela de propriedades da camada se ela for encontrada.

        :param index: O índice do modelo que representa o item selecionado no QTreeView.
        """
        # Obtém o ID da camada do item selecionado no treeView
        layer_id = index.model().itemFromIndex(index).data(Qt.ItemDataRole.UserRole)
        
        # Busca a camada correspondente no projeto QGIS usando o ID
        layer = QgsProject.instance().mapLayer(layer_id)
        
        # Se a camada for encontrada, exibe a janela de propriedades da camada
        if layer:
            self.iface.showLayerProperties(layer)

    def escolher_local_para_salvar(self, nome_padrao, tipo_arquivo):
        """
        Permite ao usuário escolher um local e um nome de arquivo para salvar uma camada, usando uma caixa de diálogo.
        O método também gerencia nomes de arquivos para evitar sobreposição e lembra o último diretório utilizado.

        Funções e Ações Desenvolvidas:
        - Recuperação do último diretório utilizado através das configurações do QGIS.
        - Geração de um nome de arquivo único para evitar sobreposição.
        - Exibição de uma caixa de diálogo para escolha do local de salvamento.
        - Atualização do último diretório utilizado nas configurações do QGIS.

        :param nome_padrao: Nome padrão proposto para o arquivo a ser salvo.
        :param tipo_arquivo: Descrição do tipo de arquivo para a caixa de diálogo (ex. "Arquivos DXF (*.dxf)").

        :return: O caminho completo do arquivo escolhido para salvar ou None se nenhum arquivo foi escolhido.
        """
        # Acessa as configurações do QGIS para recuperar o último diretório utilizado
        settings = QSettings()
        lastDir = settings.value("lastDir", "")  # Usa uma string vazia como padrão se não houver último diretório

        # Configura as opções da caixa de diálogo para salvar arquivos
        options = QFileDialog.Options()
        
        # Gera um nome de arquivo com um sufixo numérico caso o arquivo já exista
        base_nome_padrao, extensao = os.path.splitext(nome_padrao)
        numero = 1
        nome_proposto = base_nome_padrao
        
        # Incrementa o número no nome até encontrar um nome que não exista
        while os.path.exists(os.path.join(lastDir, nome_proposto + extensao)):
            nome_proposto = f"{base_nome_padrao}_{numero}"
            numero += 1

        # Propõe o nome completo no último diretório utilizado
        nome_completo_proposto = os.path.join(lastDir, nome_proposto + extensao)

        # Exibe a caixa de diálogo para salvar arquivos com o nome proposto
        fileName, _ = QFileDialog.getSaveFileName(
            self.dlg,
            "Salvar Camada",
            nome_completo_proposto,
            tipo_arquivo,
            options=options)

        # Verifica se um nome de arquivo foi escolhido
        if fileName:
            # Atualiza o último diretório usado nas configurações do QGIS
            settings.setValue("lastDir", os.path.dirname(fileName))

            # Assegura que o arquivo tenha a extensão correta
            if not fileName.endswith(extensao):
                fileName += extensao

        return fileName  # Retorna o caminho completo do arquivo escolhido ou None se cancelado

    def mostrar_mensagem(self, texto, tipo, duracao=3, caminho_pasta=None, caminho_arquivo=None):
        """
        Exibe mensagens na barra do QGIS (iface.messageBar).
        - Erro: crítica
        - Informação: informativa
        - Aviso: warning
        - Sucesso: mensagem com botões opcionais para abrir pasta e/ou arquivo
          (compatível com Windows, Linux e macOS via QDesktopServices.openUrl)
        """
        bar = iface.messageBar()  # acessa a barra de mensagens do QGIS

        # Mensagem de erro
        if tipo == "Erro":
            bar.pushMessage("Erro", texto, level=Qgis.MessageLevel.Critical, duration=duracao)
            return

        # Mensagem informativa
        if tipo in ("Informação", "Info"):
            bar.pushMessage("Informação", texto, level=Qgis.MessageLevel.Info, duration=duracao)
            return

        # Mensagem de aviso
        if tipo in ("Aviso", "Alerta", "Warning"):
            bar.pushMessage("Aviso", texto, level=Qgis.MessageLevel.Warning, duration=duracao)
            return

        # Mensagem de sucesso com botões extras
        if tipo == "Sucesso":
            msg = bar.createMessage("Sucesso", texto)

            # Botão para abrir a pasta (explorador de arquivos)
            if caminho_pasta:
                botao_abrir_pasta = QPushButton("Abrir Pasta")

                def abrir_pasta():
                    # converte o caminho para URL local e abre no sistema
                    url = QUrl.fromLocalFile(str(pathlib.Path(caminho_pasta)))
                    QDesktopServices.openUrl(url)

                botao_abrir_pasta.clicked.connect(abrir_pasta)
                msg.layout().insertWidget(1, botao_abrir_pasta)

            # Botão para abrir/executar o arquivo
            if caminho_arquivo:
                botao_executar = QPushButton("Executar")

                def abrir_arquivo():
                    # converte o caminho para URL local e abre no programa padrão
                    url = QUrl.fromLocalFile(str(pathlib.Path(caminho_arquivo)))
                    QDesktopServices.openUrl(url)

                botao_executar.clicked.connect(abrir_arquivo)
                msg.layout().insertWidget(2, botao_executar)

            # adiciona o widget final na barra do QGIS
            bar.pushWidget(msg, level=Qgis.MessageLevel.Info, duration=duracao)

    def iniciar_progress_bar(self, total_steps, titulo="Exportando malha..."):
        """
        Cria e exibe uma barra de progresso na barra de mensagens do QGIS.

        Parâmetros
       
        total_steps : int
            Número total de etapas (valor máximo da barra de progresso).
        titulo : str, opcional
            Texto exibido na mensagem junto à barra (padrão: "Exportando malha...").

        Retorna
       ----
        (QProgressBar, QWidget)
            A barra de progresso criada e o widget de mensagem associado.
        """
        # Cria a mensagem na barra do QGIS com o título fornecido
        progressMessageBar = self.iface.messageBar().createMessage(titulo)

        # Cria a barra de progresso Qt
        progressBar = QProgressBar()
        progressBar.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)  # alinha texto à esquerda/verticalmente
        progressBar.setFormat("%p% - %v de %m etapas concluídas")  # formato do texto exibido
        progressBar.setMinimumWidth(300)  # largura mínima em px

        # Define o estilo visual da barra
        progressBar.setStyleSheet("""
            QProgressBar {
                border: 1px solid grey;
                border-radius: 2px;
                background-color: #cddbde;
                text-align: center;
            }
            QProgressBar::chunk {
                background-color: #55aaff;
                width: 5px;
                margin: 1px;
            }
            QProgressBar { min-height: 5px; }""")

        # Adiciona a barra ao layout da mensagem
        progressMessageBar.layout().addWidget(progressBar)

        # Publica a mensagem com a barra na barra do QGIS
        self.iface.messageBar().pushWidget(progressMessageBar, Qgis.MessageLevel.Info)

        # Define o número máximo de etapas para a barra
        progressBar.setMaximum(total_steps)

        # Retorna a barra e o widget de mensagem (para poder atualizar/fechar depois)
        return progressBar, progressMessageBar

    def mostrar_dialogo_exporta_malha_3d(self):
        """
        Exibe o diálogo para exportação da malha 3D e conecta os botões para exportação em diferentes formatos.

        Funções e Ações Desenvolvidas:
        - Cria e exibe o diálogo ExportaMalha3D.
        - Conecta os botões do diálogo às funções de exportação correspondentes (DXF, DAE, OBJ, STL).
        - Executa o diálogo de exportação de malha 3D.

        """
        # Cria uma instância do diálogo ExportaMalha3D passando o diálogo principal como pai
        self.dlg_exporta_malha = ExportaMalha3D(self.dlg)
        
        # Conecta o botão DXF à função de exportação de malha para DXF
        self.dlg_exporta_malha.button_dxf.clicked.connect(lambda: self.exportar_malha("DXF"))
        
        # Conecta o botão DAE à função de exportação de malha para DAE
        self.dlg_exporta_malha.button_dae.clicked.connect(lambda: self.exportar_malha("DAE"))
        
        # Conecta o botão OBJ à função de exportação de malha para OBJ
        self.dlg_exporta_malha.button_obj.clicked.connect(lambda: self.exportar_malha("OBJ"))
        
        # Conecta o botão STL à função de exportação de malha para STL
        self.dlg_exporta_malha.button_stl.clicked.connect(lambda: self.exportar_malha("STL"))
        
        # Executa o diálogo de exportação de malha 3D
        self.dlg_exporta_malha.exec()

    def convert_mesh_to_polygons_and_points(self, mesh_layer):
        """
        Converte uma camada de malha para polígonos e extrai os pontos com coordenadas Z.

        A função usa algoritmos de processamento nativos do QGIS para exportar faces da malha como polígonos e vértices da malha como pontos.
        Em seguida, adiciona campos ID e Altitude às feições resultantes.

        Funções e Ações Desenvolvidas:
        - Exporta as faces da malha como polígonos.
        - Adiciona um campo ID à camada de polígonos.
        - Converte a camada de polígonos para MultiSurface (MultiPolygonZ).
        - Exporta os vértices da malha como pontos.
        - Adiciona campos ID e Altitude à camada de pontos.
        - Cria um dicionário para mapear as coordenadas dos pontos às suas altitudes.

        :param mesh_layer: Camada de malha a ser convertida (QgsMeshLayer).
        :return: Uma tupla contendo a camada de polígonos MultiSurface (MultiPolygonZ) e um dicionário de valores de altitude dos pontos.
        """
        # Obter o CRS da camada de malha
        crs = mesh_layer.crs()
        
        # Configurar parâmetros para o algoritmo de processamento de polígonos
        params_polygons = {
            'INPUT': mesh_layer,
            'DATASET_GROUPS': [],  # Ajuste conforme necessário
            'DATASET_TIME': {'type': 'static'},  # Configurar o tipo de tempo como estático
            'CRS_OUTPUT': crs.toWkt(),  # Definir o CRS de saída como o mesmo da camada de malha
            'VECTOR_OPTION': 0,  # 0 para Polygons
            'OUTPUT': 'memory:Polygons from TIN Mesh'}
        
        # Executar o algoritmo de processamento para polígonos
        result_polygons = processing.run("native:exportmeshfaces", params_polygons)
        
        # Obter a camada de polígonos resultante
        poly_layer = result_polygons['OUTPUT']
        
        # Adicionar campo ID à camada de polígonos
        provider_poly = poly_layer.dataProvider()
        provider_poly.addAttributes([QgsField("ID", QMetaType.Type.Int)])
        poly_layer.updateFields()
        
        # Atribuir valores de ID para cada feição
        with edit(poly_layer):
            for i, feature in enumerate(poly_layer.getFeatures()):
                feature.setAttribute("ID", i + 1)  # ID começa em 1
                poly_layer.updateFeature(feature)
        
        # Converter a camada de polígonos para MultiSurface (MultiPolygonZ)
        multi_poly_layer = self.convert_to_multisurface(poly_layer)
        
        # Configurar parâmetros para o algoritmo de processamento de pontos
        params_points = {
            'INPUT': mesh_layer,
            'DATASET_GROUPS': [],  # Ajuste conforme necessário
            'DATASET_TIME': {'type': 'static'},  # Configurar o tipo de tempo como estático
            'CRS_OUTPUT': crs.toWkt(),  # Definir o CRS de saída como o mesmo da camada de malha
            'VECTOR_OPTION': 1,  # 1 para pontos
            'OUTPUT': 'memory:Mesh Vertices'}
        
        # Executar o algoritmo de processamento para pontos
        result_points = processing.run("native:exportmeshvertices", params_points)
        
        # Obter a camada de pontos resultante
        point_layer = result_points['OUTPUT']
        
        # Adicionar campo ID e Altimetria à camada de pontos
        provider_point = point_layer.dataProvider()
        provider_point.addAttributes([QgsField("ID", QMetaType.Type.Int), QgsField("Altitude", QMetaType.Type.Double)])
        point_layer.updateFields()
        
        # Atribuir valores de ID e Altimetria para cada feição
        point_z_values = {}
        with edit(point_layer):
            for i, feature in enumerate(point_layer.getFeatures()):
                geom = feature.geometry()
                if QgsWkbTypes.hasZ(geom.wkbType()):  # Verificar se a geometria tem coordenada Z
                    point = geom.constGet()  # Obter a geometria como QgsPoint
                    feature.setAttribute("ID", i + 1)  # ID começa em 1
                    feature.setAttribute("Altitude", point.z())  # Define a altimetria (z-coordinate)
                    point_z_values[(point.x(), point.y())] = point.z() # Adicionar ao dicionário de valores de Z
                    point_layer.updateFeature(feature) # Atualizar feição
        # Retornar a camada de polígonos e o dicionário de valores de Z
        return multi_poly_layer, point_z_values 

    def convert_to_multisurface(self, poly_layer):
        """
        Converte uma camada de polígonos simples em uma camada de MultiSurface (MultiPolygonZ).

        A função cria uma nova camada de memória que armazena polígonos como MultiPolygonZ, preservando os atributos e IDs das feições originais.

        Funções e Ações Desenvolvidas:
        - Cria uma nova camada de MultiSurface (MultiPolygonZ) com o mesmo CRS da camada de entrada.
        - Adiciona um campo ID à nova camada.
        - Converte cada feição da camada de polígonos simples para MultiPolygon e adiciona à nova camada.
        
        :param poly_layer: Camada de polígonos simples a ser convertida (QgsVectorLayer).
        :return: A nova camada de MultiSurface (MultiPolygonZ) (QgsVectorLayer).
        """
        # Criar uma nova camada de MultiSurface (MultiPolygonZ)
        multi_poly_layer = QgsVectorLayer(f"MultiPolygonZ?crs={poly_layer.crs().authid()}", "MultiSurface Polygons", "memory")
        provider_multi_poly = multi_poly_layer.dataProvider()
        
        # Adicionar campo ID à nova camada
        provider_multi_poly.addAttributes([QgsField("ID", QMetaType.Type.Int)])
        multi_poly_layer.updateFields()
        
        # Converter cada feição para MultiPolygon e adicionar à nova camada
        with edit(multi_poly_layer):
            for feature in poly_layer.getFeatures():
                geom = feature.geometry() # Obter a geometria da feição
                if geom.isMultipart():
                    multi_geom = geom # Se a geometria já for multipart, mantê-la como está
                else:
                    multi_geom = QgsGeometry.fromMultiPolygonXY([geom.asPolygon()])  # Converter para MultiPolygon
                new_feature = QgsFeature()  # Criar uma nova feição
                new_feature.setGeometry(multi_geom)  # Definir a geometria da nova feição
                new_feature.setAttributes(feature.attributes())  # Copiar atributos da feição original
                multi_poly_layer.addFeature(new_feature)  # Adicionar a nova feição à camada
        
        return multi_poly_layer  # Retornar a nova camada de MultiSurface (MultiPolygonZ)

    def criar_simbologia_gradiente(self, valores):
        """
        Cria uma simbologia gradiente de vermelho, laranja, amarelo e verde baseada nos valores fornecidos.

        Funções e Ações Desenvolvidas:
        - Calcula os valores mínimo e máximo da lista fornecida.
        - Interpola cores entre vermelho, laranja, amarelo e verde com base nos valores fornecidos.
        - Cria um dicionário onde as chaves são os valores fornecidos e os valores são listas RGBA.

        :param valores: Lista de valores para os quais a simbologia será aplicada.
        :return: Um dicionário com valores como chave e a cor RGBA correspondente como valor.
        """
        cores = {}  # Dicionário para armazenar as cores
        min_valor = min(valores)  # Calcula o valor mínimo da lista
        max_valor = max(valores)  # Calcula o valor máximo da lista

        # Itera sobre cada valor na lista de valores
        for valor in valores:
            ratio = (valor - min_valor) / (max_valor - min_valor)  # Calcula a razão normalizada do valor

            # Interpola a cor com base na razão
            if ratio < 0.33:
                # Interpolação de vermelho para laranja
                r = 1.0  # Vermelho constante
                g = ratio * 3.0  # Verde cresce de 0 a 1
                b = 0.0  # Azul constante
            elif ratio < 0.66:
                # Interpolação de laranja para amarelo
                r = 1.0 - (ratio - 0.33) * 3.0  # Vermelho decresce de 1 a 0
                g = 1.0  # Verde constante
                b = 0.0  # Azul constante
            else:
                # Interpolação de amarelo para verde
                r = 0.0  # Vermelho constante
                g = 1.0  # Verde constante
                b = (ratio - 0.66) * 3.0  # Azul cresce de 0 a 1
            a = 1.0  # Alfa constante
            cores[valor] = [r, g, b, a]  # Adiciona a cor interpolada ao dicionário

        return cores  # Retorna o dicionário de cores

    def abrir_visualizador_malha_3d(self):
        """
        Visualização 3D sem travar a UI:
          1) Mostra barra INDETERMINADA de imediato;
          2) Roda PrepTask (mesh -> polygons + dict Z) em background;
          3) Ao terminar, inicia BuffersWorker (triangulação) em QThread;
          4) Quando BuffersWorker informar o total, troca a barra para DETERMINADA;
          5) Abre VisualizadorDAE3D direto dos buffers; fallback: DAE em disco.
        """
        # 0) Resolve camada selecionada
        indexes = self.dlg.treeViewListaMalha.selectedIndexes()
        if not indexes:
            self.mostrar_mensagem("Nenhuma camada selecionada", "Erro"); return
        selected_layer_name = self.treeViewModel.itemFromIndex(indexes[0]).text()
        layers = QgsProject.instance().mapLayersByName(selected_layer_name)
        if not layers:
            self.mostrar_mensagem("Camada não encontrada", "Erro"); return
        layer = layers[0]
        if not isinstance(layer, QgsMeshLayer):
            self.mostrar_mensagem("A camada selecionada não é uma malha", "Erro"); return

        msg_bar = self.iface.messageBar()

        # 1) Mensagem + barra INDETERMINADA imediata
        prep_item = msg_bar.createMessage("Aguarde", "Preparando malha para visualização…")
        prep_bar = QProgressBar(); prep_bar.setRange(0, 0)  # indeterminada
        prep_item.layout().addWidget(prep_bar)
        msg_bar.pushWidget(prep_item, Qgis.MessageLevel.Info)

        # manter refs vivas
        self._viewer_msg_item = prep_item
        self._viewer_msg_bar  = prep_bar

        def dismiss_bar():
            try:
                msg_bar.popWidget(prep_item)
            except Exception:
                try: prep_item.close()
                except Exception: pass
            self._viewer_msg_item = None
            self._viewer_msg_bar  = None

        # 2) Deferir 1 tick para a barra aparecer antes de começar a preparar
        def _start_prep_task():
            try:
                # Task de preparação (background)
                task = PrepTask(self, layer.id())
                self._viewer_prep_task = task  # guardar para GC/cancel

                # Quando a preparação terminar com sucesso → inicia triangulação
                def on_prep_ok(polygons, point_z_values):
                    # 3) Worker de buffers (triangulação) em QThread
                    thread = QThread(self.iface.mainWindow())
                    worker = BuffersWorker(polygons, point_z_values)
                    worker.moveToThread(thread)

                    # manter vivos
                    self._viewer_thread = thread
                    self._viewer_worker = worker

                    # ao receber total de faces → barra DETERMINADA
                    def on_max(m):
                        m = max(1, int(m))
                        try:
                            prep_bar.setRange(0, m)
                            prep_bar.setValue(0)
                        except Exception:
                            pass

                    def on_progress(v):
                        try:
                            if prep_bar.maximum() > 0:
                                prep_bar.setValue(int(v))
                        except Exception:
                            pass

                    def on_finished(positions, tri_indices, _dur):
                        dismiss_bar()
                        try:
                            visualizador = VisualizadorDAE3D(
                                None, self.dlg, self,
                                positions=positions,
                                tri_indices=tri_indices)
                            visualizador.show()
                        except Exception as e:
                            self.mostrar_mensagem(f"Erro ao instanciar visualizador: {e}", "Erro")
                        finally:
                            try: thread.quit()
                            except Exception: pass

                    def on_error(msg):
                        # fallback: caminha via DAE em disco
                        dismiss_bar()
                        try:
                            dae_file_path = self.exportar_malha_para_dae(layer)
                            if not dae_file_path:
                                raise RuntimeError(msg or "Falha no pipeline de buffers e no fallback DAE.")
                            visualizador = VisualizadorDAE3D(dae_file_path, self.dlg, self)
                            visualizador.show()
                        except Exception as e:
                            self.mostrar_mensagem(f"Erro ao abrir visualização 3D: {e}", "Erro")
                        finally:
                            try: thread.quit()
                            except Exception: pass

                    worker.max_value.connect(on_max)
                    worker.progress.connect(on_progress)
                    worker.finished.connect(on_finished)
                    worker.error.connect(on_error)

                    thread.started.connect(worker.run)
                    worker.finished.connect(worker.deleteLater)
                    thread.finished.connect(thread.deleteLater)
                    thread.start()

                def on_prep_failed(msg):
                    dismiss_bar()
                    self.mostrar_mensagem(f"Erro na preparação da malha: {msg}", "Erro")

                task.finished_ok.connect(on_prep_ok)
                task.failed.connect(on_prep_failed)

                # dispara a task
                QgsApplication.taskManager().addTask(task)

            except Exception as e:
                dismiss_bar()
                self.mostrar_mensagem(f"Erro ao iniciar preparação: {e}", "Erro")

        QTimer.singleShot(0, _start_prep_task)

    def _to_python_mesh_data(self, faces_layer: QgsVectorLayer, verts_layer: QgsVectorLayer):
        """
        Converte:
          - faces_layer (polígonos) → List[List[List[Tuple[float,float]]]]
            (lista de polígonos; cada polígono = lista de anéis; cada anel = lista de (x,y))
          - verts_layer (pontos Z)   → dict[(x,y)] = z
        Não altera camadas; sem edição.
        """
        # Polígonos (apenas XY; Z virá dos pontos)
        polygons = []  # tipo: List[List[List[Tuple[float,float]]]]
        for feat in faces_layer.getFeatures():
            geom = feat.geometry()
            if geom.isMultipart():
                multi = geom.asMultiPolygon()  # [[[QgsPointXY]]]
                poly_list = []
                for poly in multi:
                    ring_list = []
                    for ring in poly:
                        ring_list.append([(p.x(), p.y()) for p in ring])
                    poly_list.extend([ring_list])
                polygons.extend(poly_list)
            else:
                poly = geom.asPolygon()
                ring_list = []
                for ring in poly:
                    ring_list.append([(p.x(), p.y()) for p in ring])
                polygons.append(ring_list)

        # Pontos com Z
        point_z_values = {}  # {(x,y): z}
        for feat in verts_layer.getFeatures():
            g = feat.geometry()
            if QgsWkbTypes.hasZ(g.wkbType()):
                pt = g.constGet()  # QgsPoint
                # opcional: quantizar para reduzir problemas de igualdade de float
                x = round(pt.x(), 9)
                y = round(pt.y(), 9)
                point_z_values[(x, y)] = pt.z()

        if not polygons or not point_z_values:
            raise ValueError("Sem polígonos ou sem pontos Z para exportar.")

        return polygons, point_z_values

    def _set_progressbar_max(self, maxv: int):
        # Se vier 0 (sem triângulos), evita indeterminado eterno
        maxv = maxv if maxv > 0 else 1
        # garante modo determinado
        self.iface.messageBar().currentItem().progressBar().setRange(0, maxv)

    def _set_progressbar_value(self, v: int):
        self.iface.messageBar().currentItem().progressBar().setValue(v)

    def _on_export_finished(self, fmt: str, path: str, duration: float, progressMsgBar=None, close_dialog: bool = True):
        """
        Handler genérico para término de exportação.

        :param fmt: Rótulo do formato (ex.: "STL", "OBJ", "DXF", "DAE").
        :param path: Caminho do arquivo exportado.
        :param duration: Duração em segundos.
        :param progressMsgBar: Item da barra de mensagem (se quiser manipular/fechar algo específico).
        :param close_dialog: Se True, fecha o diálogo de exportação se existir.
        """
        # Limpa a barra de mensagens/progresso
        try:
            if progressMsgBar is not None:
                self.iface.messageBar().popWidget(progressMsgBar)
        except Exception:
            try:
                if progressMsgBar is not None:
                    progressMsgBar.close()
            except Exception:
                pass

        # Mensagem de sucesso
        self.mostrar_mensagem(f"Camada exportada para {fmt} em {duration:.2f} segundos", "Sucesso", caminho_pasta=os.path.dirname(path), caminho_arquivo=path)

        # Fecha o diálogo se existir
        if close_dialog and getattr(self, "dlg_exporta_malha", None):
            self.dlg_exporta_malha.close()

    def _on_export_error(self, fmt: str, message: str, progressMsgBar=None):
        """
        Handler genérico para erro de exportação.
        """
        try:
            if progressMsgBar is not None:
                self.iface.messageBar().popWidget(progressMsgBar)
        except Exception:
            try:
                if progressMsgBar is not None:
                    progressMsgBar.close()
            except Exception:
                pass

        self.mostrar_mensagem(f"Erro ao exportar {fmt}: {message}", "Erro")

    def _start_export_worker(self, fmt: str, worker_cls, polygons, point_z_values, save_path: str, progressBar, progressMsgBar, **worker_kwargs):
        """
        Inicia um worker de exportação (DAE/DXF/OBJ/STL) em QThread, conectando sinais
        de progresso e finalização de forma genérica.

        :param fmt: Rótulo do formato ("DAE" | "DXF" | "OBJ" | "STL").
        :param worker_cls: Classe do worker (ex.: DaeExportWorker).
        :param polygons: Dados puros de polígonos (lista de anéis).
        :param point_z_values: Dict[(x,y)] = z.
        :param save_path: Caminho do arquivo de saída.
        :param progressBar: QProgressBar mostrado na QgsMessageBar.
        :param progressMsgBar: QgsMessageBarItem (para opcional cancel/fechamento).
        :param worker_kwargs: kwargs extras para o worker (ex.: binary=True no STL).
        """
        # Criamos contêiner para manter as referências vivas (evita GC)
        if not hasattr(self, "_live_exports"):
            self._live_exports = []

        thread = QThread(self.iface.mainWindow())
        worker = worker_cls(polygons, point_z_values, save_path, **worker_kwargs)
        worker.moveToThread(thread)

        # Ligações de execução e progresso
        thread.started.connect(worker.run)
        worker.max_value.connect(lambda m: progressBar.setRange(0, max(1, m)))
        worker.progress.connect(progressBar.setValue)

        # Finalização/erro genéricos (use seus handlers unificados)
        # ATENÇÃO: garanta que sua assinatura seja _on_export_finished(fmt, path, duration, progressMsgBar=None, ...)
        worker.finished.connect(lambda p, d: self._on_export_finished(fmt, p, d, progressMsgBar))
        worker.error.connect(lambda msg: self._on_export_error(fmt, msg, progressMsgBar))

        # Limpeza Qt
        worker.finished.connect(thread.quit)
        worker.finished.connect(worker.deleteLater)
        thread.finished.connect(thread.deleteLater)

        # Cancelamento pelo X da barra (se existir)
        if hasattr(progressMsgBar, "accepted"):
            progressMsgBar.accepted.connect(worker.cancel)

        # Registrar bundle vivo e removê‑lo ao terminar (mantém refs)
        bundle = {"fmt": fmt, "thread": thread, "worker": worker, "bar": progressBar, "item": progressMsgBar}
        self._live_exports.append(bundle)

        def _remove_bundle():
            try:
                self._live_exports.remove(bundle)
            except Exception:
                pass
        thread.finished.connect(_remove_bundle)

        # start
        thread.start()

    def exportar_malha_kml(self):
        """
        Exporta a camada de malha selecionada para o formato KML.

        Fluxo:
        1. Abre diálogo para configurar estilo (cores, transparência etc.).
        2. Obtém a camada de malha selecionada na interface.
        3. Pede ao usuário o caminho/arquivo de saída (.kml).
        4. Exibe mensagem inicial e cria barra de progresso.
        5. Inicia o processo de conversão e exportação assíncrona.
        """
        # 1) Diálogo de estilo
        style_dialog = KMLStyleDialog()
        if not style_dialog.exec():
            return
        style_options = style_dialog.get_style_options()

        # 2) Seleção da camada
        indexes = self.dlg.treeViewListaMalha.selectedIndexes()
        if not indexes:
            self.mostrar_mensagem("Nenhuma camada selecionada", "Erro"); return
        selected_layer_name = self.treeViewModel.itemFromIndex(indexes[0]).text()
        layers = QgsProject.instance().mapLayersByName(selected_layer_name)
        if not layers:
            self.mostrar_mensagem("Camada não encontrada", "Erro"); return
        layer = layers[0]
        if not isinstance(layer, QgsMeshLayer):
            self.mostrar_mensagem("A camada selecionada não é uma malha", "Erro"); return

        # 3) Caminho de saída
        save_path = self.escolher_local_para_salvar(layer.name() + ".kml", "KML Files (*.kml)")
        if not save_path:
            return

        # 4) Mensagem inicial + Barra com título correto
        self.mostrar_mensagem("Aguarde, iniciando exportação da camada para KML", "Informação", duracao=2)

        progressBar, progressMsgBar = self.iniciar_progress_bar(0, titulo="Exportando malha para KML")
        progressBar.setRange(0, 0)  # indeterminado durante a conversão

        # 5) Conversão + start worker
        self._kml_convert_and_export_with_tasks(layer, save_path, progressBar, progressMsgBar, style_options)

    def _kml_convert_and_export_with_tasks(self, mesh_layer, save_path, progressBar, progressMsgBar, style_options):
        """
        KML:
        - Cria APENAS a task das FACES (assíncrona, com descrição controlada)
        - Ao concluir faces, exporta VÉRTICES de forma síncrona (processing.run)
          → não aparece a notificação automática do QGIS para os vértices
        - Converte para Python, reprojeta p/ WGS84 e inicia KmlExportWorker
        """
        # Diretório temporário
        self._kml_tmpdir = tempfile.mkdtemp(prefix="kmlconv_")
        faces_path = os.path.join(self._kml_tmpdir, "faces.gpkg")
        verts_path = os.path.join(self._kml_tmpdir, "verts.gpkg")

        def _cleanup():
            if getattr(self, "_kml_tmpdir", None):
                try:
                    shutil.rmtree(self._kml_tmpdir)
                except Exception:
                    pass
                self._kml_tmpdir = None
            for attr in ("_kml_context", "_kml_feedback", "_task_faces_kml"):
                if hasattr(self, attr):
                    setattr(self, attr, None)

        def _on_error(msg):
            try:
                self.iface.messageBar().popWidget(wait_item)
            except Exception:
                try: wait_item.close()
                except Exception: pass
            self.mostrar_mensagem(msg, "Erro")
            _cleanup()

        # Parâmetros
        crs_wkt = mesh_layer.crs().toWkt()
        params_faces = {
            'INPUT': mesh_layer, 'DATASET_GROUPS': [], 'DATASET_TIME': {'type': 'static'},
            'CRS_OUTPUT': crs_wkt, 'VECTOR_OPTION': 0, 'OUTPUT': faces_path}
        params_verts = {
            'INPUT': mesh_layer, 'DATASET_GROUPS': [], 'DATASET_TIME': {'type': 'static'},
            'CRS_OUTPUT': crs_wkt, 'VECTOR_OPTION': 1, 'OUTPUT': verts_path}

        # Algoritmo (apenas faces como task)
        reg = QgsApplication.processingRegistry()
        alg_faces = reg.createAlgorithmById("native:exportmeshfaces")
        if not alg_faces:
            _on_error("Algoritmo 'native:exportmeshfaces' não encontrado.")
            return

        # Contexto/feedback fortes
        self._kml_context  = QgsProcessingContext()
        self._kml_feedback = QgsProcessingFeedback()

        # Task SOMENTE das FACES (com descrição desejada)
        self._task_faces_kml = QgsProcessingAlgRunnerTask(alg_faces, params_faces, self._kml_context, self._kml_feedback)
        self._task_faces_kml.setDescription('Exporta malha de faces')

        # Callback das faces
        def _on_faces_executed(success, results):
            if not success:
                _on_error("Falha ao exportar faces da malha (native:exportmeshfaces).")
                return

            # Exportar VÉRTICES sincronamente (sem task → sem notificação automática)
            try:
                processing.run(
                    "native:exportmeshvertices",
                    params_verts,
                    context=self._kml_context,
                    feedback=self._kml_feedback)
            except Exception as e:
                _on_error(f"Falha ao exportar vértices da malha (native:exportmeshvertices): {e}")
                return

            # Carregar GPKGs
            faces_vl = QgsVectorLayer(faces_path, "faces_tmp", "ogr")
            verts_vl = QgsVectorLayer(verts_path, "verts_tmp", "ogr")
            if not faces_vl.isValid() or not verts_vl.isValid():
                _on_error("Falha ao carregar arquivos temporários (GPKG).")
                return

            # Converter p/ Python + reprojetar p/ WGS84 + estilo
            try:
                polygons_xy, point_z_values = self._to_python_mesh_data(faces_vl, verts_vl)  # XY + Z por ponto
                polygons_wgs84_3d = self._polygons_to_wgs84_3d(polygons_xy, point_z_values, mesh_layer.crs())  # (lon,lat,z)
                style_payload = self._kml_style_payload(style_options)
            except Exception as e:
                _on_error(f"Falha na preparação para KML: {e}")
                return

            # Barra determinada; worker definirá max/valor
            progressBar.setRange(0, 1)

            # Dispara worker KML (usa polígonos já em (lon,lat,z) e estilo ABGR)
            self._start_export_worker(
                fmt="KML",
                worker_cls=KmlExportWorker,
                polygons=polygons_wgs84_3d,
                point_z_values={},      # não usado pelo KML
                save_path=save_path,
                progressBar=progressBar,
                progressMsgBar=progressMsgBar,
                style=style_payload)

            _cleanup()

        # Conecta e dispara task de FACES
        self._task_faces_kml.executed.connect(_on_faces_executed)
        QgsApplication.taskManager().addTask(self._task_faces_kml)

    def _polygons_to_wgs84_3d(self, polygons_xy, point_z_values, src_crs):
        """
        Converte 'polygons_xy' (ring de (x,y) no CRS de origem) em
        'polygons_wgs84_3d' (ring de (lon,lat,z)), usando Z do dict.
        Mantém anéis na mesma ordem; fecha anel depois no worker.
        """
        from qgis.core import QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsProject, QgsPointXY
        crs_dest = QgsCoordinateReferenceSystem(4326)
        xform = QgsCoordinateTransform(src_crs, crs_dest, QgsProject.instance())

        out = []  # List[List[List[Tuple[lon,lat,z]]]]
        for poly in polygons_xy:
            rings3d = []
            for ring in poly:
                ring3d = []
                for (x, y) in ring:
                    p_ll = xform.transform(QgsPointXY(x, y))
                    z = point_z_values.get((round(x,9), round(y,9)), 0.0)
                    ring3d.append((p_ll.x(), p_ll.y(), z))
                rings3d.append(ring3d)
            out.append(rings3d)
        return out

    def _kml_style_payload(self, style_options):
        """
        Converte QColor/opacidade em strings KML (ABGR) e demais opções.
        Espera opacidades inteiras 0–255 (como seu diálogo fornece).
        """
        def _abgr(qcolor, opacity255: int):
            # KML espera ABGR em hex: AA,BB,GG,RR
            return f"{opacity255:02x}{qcolor.blue():02x}{qcolor.green():02x}{qcolor.red():02x}"

        line_abgr = _abgr(style_options["line_color"], style_options["line_opacity"])
        poly_abgr  = _abgr(style_options["face_color"], style_options["face_opacity"])
        return {
            "line_color_abgr": line_abgr,
            "line_width": int(style_options["line_width"]),
            "poly_color_abgr": poly_abgr,
            "fill": 1,      # manter como no seu código
            "outline": 1,
            "style_id": "customStyle"}

    def _build_color_map_from_pointz(self, point_z_values: dict) -> dict:
        """
        Gera {z: (r,g,b,a)} a partir dos Zs únicos em point_z_values,
        usando UiManagerM.criar_simbologia_gradiente.
        """
        # Zs únicos e ordenados
        zs = sorted({float(z) for z in point_z_values.values()})
        if not zs:
            return {}
        # criar_simbologia_gradiente espera lista de valores
        cores_dict = self.criar_simbologia_gradiente(zs)  # retorna {valor: [r,g,b,a]}
        # normaliza: chaves float e tuplas RGBA
        color_map = {float(z): tuple(cores_dict.get(z, [1,1,1,1])) for z in zs}
        return color_map

    def _polygons_from_vector_layer(self, faces_layer: QgsVectorLayer):
        """
        Converte uma QgsVectorLayer de polígonos em
        List[List[List[Tuple[x,y]]]] (polígono → anéis → (x,y)).
        """
        polygons = []
        for feat in faces_layer.getFeatures():
            geom = feat.geometry()
            if geom.isMultipart():
                multi = geom.asMultiPolygon()
                for poly in multi:
                    rings = []
                    for ring in poly:
                        rings.append([(p.x(), p.y()) for p in ring])
                    polygons.append(rings)
            else:
                poly = geom.asPolygon()
                rings = []
                for ring in poly:
                    rings.append([(p.x(), p.y()) for p in ring])
                polygons.append(rings)
        return polygons

    def exportar_malha_para_dae(self, layer):
        """
        Exporta a malha para DAE (COLLADA) usando worker em QThread.

        Fluxo sucinto:
        - Valida a camada (bloqueia vetor em edição).
        - Mostra barra “Preparando...” e converte malha → polígonos e pontos Z.
        - Gera estruturas Python e cores via criar_simbologia_gradiente.
        - Executa DaeExportWorker (assíncrono) com barra de progresso.
        - Exibe mensagem de sucesso/erro e retorna o caminho gerado.

        Parâmetros:
        - layer (QgsMeshLayer | QgsVectorLayer): Camada selecionada.

        Retorno:
        - str | None: Caminho do .dae criado ou None em caso de falha.
        """
        try:
            # 1) bloqueio de edição em vetor
            if isinstance(layer, QgsVectorLayer) and layer.isEditable():
                self.mostrar_mensagem(f"A camada '{layer.name()}' está no modo de edição. Por favor, salve ou cancele as edições antes de exportar.", "Erro")
                return None

            # 2) caminho temp
            temp_dir = os.path.join(os.path.expanduser("~"), "Temp")
            os.makedirs(temp_dir, exist_ok=True)
            dae_file_path = os.path.join(temp_dir, layer.name() + ".dae")

            # 3) MOSTRAR barra de “Preparando...” ANTES da conversão pesada
            msg_bar = self.iface.messageBar()
            wait_item = msg_bar.createMessage("Aguarde", "Preparando malha para exportação...")
            progress_bar = QProgressBar()
            progress_bar.setRange(0, 0)  # indeterminado
            wait_item.layout().addWidget(progress_bar)
            msg_bar.pushWidget(wait_item, Qgis.MessageLevel.Info)

            try:
                # 4) conversão pesada (síncrona) — aqui era a “travada”
                polygon_layer, point_z_values = self.convert_mesh_to_polygons_and_points(layer)
            finally:
                # remove só este item, sem apagar outras barras em uso
                try:
                    msg_bar.popWidget(wait_item)
                except Exception:
                    # fallback se popWidget não estiver disponível
                    try:
                        self.iface.messageBar().popWidget(wait_item)
                    except Exception:
                        try: wait_item.close()
                        except Exception: pass

            if not polygon_layer or not point_z_values:
                self.mostrar_mensagem("Falha na conversão da malha para polígonos e pontos", "Erro")
                return None

            # 5) montar estruturas Python e color_map a partir da sua simbologia
            polygons = self._polygons_from_vector_layer(polygon_layer)
            color_map = self._build_color_map_from_pointz(point_z_values)  # usa criar_simbologia_gradiente internamente

            # 6) rodar DaeExportWorker (com color_map) usando QThread + QEventLoop
            from qgis.PyQt.QtCore import QThread, QEventLoop
            thread = QThread(self.iface.mainWindow())
            worker = DaeExportWorker(polygons, point_z_values, dae_file_path, color_map=color_map)
            worker.moveToThread(thread)

            done = {"ok": False, "duration": 0.0, "err": None}
            loop = QEventLoop()

            def _on_finished(path, dur):
                done["ok"] = True
                done["duration"] = dur
                loop.quit()

            def _on_error(msg):
                done["err"] = msg
                loop.quit()

            thread.started.connect(worker.run)
            worker.finished.connect(_on_finished)
            worker.error.connect(_on_error)
            worker.finished.connect(thread.quit)
            worker.finished.connect(worker.deleteLater)
            thread.finished.connect(thread.deleteLater)

            # barra de progresso da escrita (opcional)
            progressBar, progressMsgBar = self.iniciar_progress_bar(0)
            worker.max_value.connect(lambda m: progressBar.setRange(0, max(1, m)))
            worker.progress.connect(progressBar.setValue)

            thread.start()
            loop.exec()  # mantém UI responsiva e espera terminar

            # limpa a barra de progresso da escrita
            try:
                self.iface.messageBar().popWidget(wait_item)
            except Exception:
                try: wait_item.close()
                except Exception: pass

            if done["err"]:
                self.mostrar_mensagem(f"Erro ao exportar a malha para DAE: {done['err']}", "Erro")
                return None

            self.mostrar_mensagem(f"Camada exportada para DAE em {done['duration']:.2f} segundos", "Sucesso", caminho_pasta=os.path.dirname(dae_file_path), caminho_arquivo=dae_file_path)
            return dae_file_path

        except QgsProcessingException as pe:
            self.mostrar_mensagem(f"Erro ao exportar a malha para DAE: {str(pe)}. Verifique o modo de edição ou as restrições da camada.", "Erro")
            return None
        except Exception as e:
            self.mostrar_mensagem(f"Erro inesperado: {str(e)}", "Erro")
            return None

    def exportar_malha(self, formato):
        """
        Exporta a camada de malha selecionada para o formato informado.

        Resumo do fluxo:
        - Valida a seleção no QTreeView e o tipo da camada (QgsMeshLayer).
        - Abre o diálogo para escolher o caminho de saída com a extensão do formato.
        - Para formatos suportados (DAE/DXF/OBJ/STL), prepara os dados e inicia a exportação assíncrona
          (conversão via Processing → estruturas Python → gravação em QThread), exibindo barra de progresso.
        - Mostra mensagens de sucesso/erro e fecha o diálogo, quando aplicável.

        Parâmetros:
        - formato (str): "DAE", "DXF", "OBJ" ou "STL".

        Retorno:
        - None (opera por efeitos na UI e criação do arquivo no caminho escolhido).
        """
        start_time = time.time()
        indexes = self.dlg.treeViewListaMalha.selectedIndexes()
        if not indexes:
            self.mostrar_mensagem("Nenhuma camada selecionada", "Erro")
            return

        selected_layer_name = self.treeViewModel.itemFromIndex(indexes[0]).text()
        layers = QgsProject.instance().mapLayersByName(selected_layer_name)
        if not layers:
            self.mostrar_mensagem("Camada não encontrada", "Erro")
            return

        layer = layers[0]
        if not isinstance(layer, QgsMeshLayer):
            self.mostrar_mensagem("A camada selecionada não é uma malha", "Erro")
            return

        if formato == "DAE":
            save_path = self.escolher_local_para_salvar(layer.name() + ".dae", "DAE Files (*.dae)")
            if not save_path:
                return

            # Mensagem inicial
            self.mostrar_mensagem("Aguarde, iniciando exportação da camada para DAE", "Informação", duracao=2)

            # Barra com título correto
            progressBar, progressMsgBar = self.iniciar_progress_bar(0, titulo="Exportando malha para DAE")
            progressBar.setRange(0, 0)  # indeterminado

            self._dae_convert_and_export_with_tasks(layer, save_path, progressBar, progressMsgBar)

        elif formato == "DXF":
            save_path = self.escolher_local_para_salvar(layer.name() + ".dxf", "DXF Files (*.dxf)")
            if not save_path:
                return

            # 1) Mensagem inicial
            self.mostrar_mensagem("Aguarde, iniciando exportação da camada para DXF", "Informação", duracao=2)

            # 2) Barra de progresso com o título correto
            progressBar, progressMsgBar = self.iniciar_progress_bar(0, titulo="Exportando malha para DXF")
            progressBar.setRange(0, 0)  # indeterminado durante a conversão

            # 3) Dispara o pipeline DXF
            self._dxf_convert_and_export_with_tasks(layer, save_path, progressBar, progressMsgBar)

        elif formato == "OBJ":
            save_path = self.escolher_local_para_salvar(layer.name() + ".obj", "OBJ Files (*.obj)")
            if not save_path:
                return

            self.mostrar_mensagem("Aguarde, iniciando exportação da camada para OBJ", "Informação", duracao=2)

            progressBar, progressMsgBar = self.iniciar_progress_bar(0, titulo="Exportando malha para OBJ")
            progressBar.setRange(0, 0)  # indeterminado

            self._obj_convert_and_export_with_tasks(layer, save_path, progressBar, progressMsgBar)

        elif formato == "STL":
            save_path = self.escolher_local_para_salvar(layer.name() + ".stl", "STL Files (*.stl)")
            if not save_path:
                return

            self.mostrar_mensagem("Aguarde, iniciando exportação da camada para STL", "Informação", duracao=2)

            progressBar, progressMsgBar = self.iniciar_progress_bar(0, titulo="Exportando malha para STL")
            progressBar.setRange(0, 0)

            self._stl_convert_and_export_with_tasks(layer, save_path, progressBar, progressMsgBar)

        else:
            super().exportar_malha(formato)

    def _dxf_convert_and_export_with_tasks(self, mesh_layer, save_path, progressBar, progressMsgBar):
        """
        DXF:
        - Cria APENAS a task das FACES (assíncrona)
        - Ao concluir faces, exporta VÉRTICES de forma síncrona (processing.run)
          → não aparece a notificação automática do QGIS para os vértices
        - Converte GPKG → Python e inicia DxfExportWorker
        """
        # 1) Barra indeterminada durante a conversão inicial
        progressBar.setRange(0, 0)

        # 2) Tempdir guardado em self
        self._dxf_tmpdir = tempfile.mkdtemp(prefix="dxfconv_")
        faces_path = os.path.join(self._dxf_tmpdir, "faces.gpkg")
        verts_path = os.path.join(self._dxf_tmpdir, "verts.gpkg")

        def _cleanup():
            if getattr(self, "_dxf_tmpdir", None):
                try:
                    shutil.rmtree(self._dxf_tmpdir)
                except Exception:
                    pass
                self._dxf_tmpdir = None
            for attr in ("_dxf_context", "_dxf_feedback", "_task_faces_dxf"):
                if hasattr(self, attr):
                    setattr(self, attr, None)

        def _on_error(msg):
            try:
                self.iface.messageBar().popWidget(wait_item)
            except Exception:
                try: wait_item.close()
                except Exception: pass
            self.mostrar_mensagem(msg, "Erro")
            _cleanup()

        # 3) Parâmetros de exportação
        crs_wkt = mesh_layer.crs().toWkt()
        params_faces = {
            'INPUT': mesh_layer,
            'DATASET_GROUPS': [],
            'DATASET_TIME': {'type': 'static'},
            'CRS_OUTPUT': crs_wkt,
            'VECTOR_OPTION': 0,   # polygons
            'OUTPUT': faces_path}
        params_verts = {
            'INPUT': mesh_layer,
            'DATASET_GROUPS': [],
            'DATASET_TIME': {'type': 'static'},
            'CRS_OUTPUT': crs_wkt,
            'VECTOR_OPTION': 1,   # points
            'OUTPUT': verts_path}

        # 4) Algoritmos
        reg = QgsApplication.processingRegistry()
        alg_faces = reg.createAlgorithmById("native:exportmeshfaces")
        if not alg_faces:
            _on_error("Algoritmo 'native:exportmeshfaces' não encontrado.")
            return

        # 5) Contexto/feedback fortes em self
        self._dxf_context  = QgsProcessingContext()
        self._dxf_feedback = QgsProcessingFeedback()

        # 6) Task SOMENTE para FACES
        self._task_faces_dxf = QgsProcessingAlgRunnerTask(alg_faces, params_faces, self._dxf_context, self._dxf_feedback)
        # Define a descrição para controlar o texto da notificação de conclusão
        self._task_faces_dxf.setDescription('Exporta malha de faces')

        # 7) Callback das faces
        def _on_faces_executed(success, results):
            if not success:
                _on_error("Falha ao exportar faces da malha (native:exportmeshfaces).")
                return

            # 7.1) Exporta VÉRTICES sincronamente (sem criar task → sem notificação automática)
            try:
                processing.run(
                    "native:exportmeshvertices",
                    params_verts,
                    context=self._dxf_context,
                    feedback=self._dxf_feedback)
            except Exception as e:
                _on_error(f"Falha ao exportar vértices da malha (native:exportmeshvertices): {e}")
                return

            # 7.2) Carrega GPKGs e converte para dados Python
            faces_vl = QgsVectorLayer(faces_path, "faces_tmp", "ogr")
            verts_vl = QgsVectorLayer(verts_path, "verts_tmp", "ogr")
            if not faces_vl.isValid() or not verts_vl.isValid():
                _on_error("Falha ao carregar arquivos temporários gerados (GPKG).")
                return

            try:
                polygons, point_z_values = self._to_python_mesh_data(faces_vl, verts_vl)
            except Exception as e:
                _on_error(f"Falha ao converter dados para Python: {e}")
                return

            # 7.3) Barra volta para modo determinado; o worker ajustará o máximo real
            progressBar.setRange(0, 1)

            # 7.4) Inicia o worker de DXF
            self._start_export_worker(fmt="DXF", worker_cls=DxfExportWorker, polygons=polygons, point_z_values=point_z_values, save_path=save_path, progressBar=progressBar, progressMsgBar=progressMsgBar)

            # 7.5) Limpeza final
            _cleanup()

        # 8) Conecta e dispara a task de FACES
        self._task_faces_dxf.executed.connect(_on_faces_executed)
        QgsApplication.taskManager().addTask(self._task_faces_dxf)

    def _dae_convert_and_export_with_tasks(self, mesh_layer, save_path, progressBar, progressMsgBar):
        """
        DAE:
        - Cria APENAS a task das FACES (assíncrona, com descrição controlada)
        - Ao concluir faces, exporta VÉRTICES de forma síncrona (processing.run)
          → não aparece a notificação automática do QGIS para os vértices
        - Converte GPKG → Python e inicia DaeExportWorker
        """
        # 1) Barra indeterminada enquanto converte
        progressBar.setRange(0, 0)

        # 2) Tempdir guardado em self (limpar no final)
        self._dae_tmpdir = tempfile.mkdtemp(prefix="daeconv_")
        faces_path = os.path.join(self._dae_tmpdir, "faces.gpkg")
        verts_path = os.path.join(self._dae_tmpdir, "verts.gpkg")

        def _cleanup():
            if getattr(self, "_dae_tmpdir", None):
                try:
                    shutil.rmtree(self._dae_tmpdir)
                except Exception:
                    pass
                self._dae_tmpdir = None
            for attr in ("_proc_context", "_proc_feedback", "_task_faces"):
                if hasattr(self, attr):
                    setattr(self, attr, None)

        def _on_error(msg):
            try:
                self.iface.messageBar().popWidget(wait_item)
            except Exception:
                try: wait_item.close()
                except Exception: pass
            self.mostrar_mensagem(msg, "Erro")
            _cleanup()

        # 3) Parâmetros
        crs_wkt = mesh_layer.crs().toWkt()
        params_faces = {
            'INPUT': mesh_layer,
            'DATASET_GROUPS': [],
            'DATASET_TIME': {'type': 'static'},
            'CRS_OUTPUT': crs_wkt,
            'VECTOR_OPTION': 0,   # polygons
            'OUTPUT': faces_path}
        params_verts = {
            'INPUT': mesh_layer,
            'DATASET_GROUPS': [],
            'DATASET_TIME': {'type': 'static'},
            'CRS_OUTPUT': crs_wkt,
            'VECTOR_OPTION': 1,   # points
            'OUTPUT': verts_path}

        # 4) Algoritmo (apenas faces como task)
        reg = QgsApplication.processingRegistry()
        alg_faces = reg.createAlgorithmById("native:exportmeshfaces")
        if not alg_faces:
            _on_error("Algoritmo 'native:exportmeshfaces' não encontrado.")
            return

        # 5) Contexto/feedback fortes (lifetime)
        self._proc_context  = QgsProcessingContext()
        self._proc_feedback = QgsProcessingFeedback()

        # 6) Task das FACES (com descrição para controlar a mensagem “Tarefa concluída: ...”)
        self._task_faces = QgsProcessingAlgRunnerTask(alg_faces, params_faces, self._proc_context, self._proc_feedback)
        self._task_faces.setDescription('Exporta malha de faces')

        # 7) Callback das faces
        def _on_faces_executed(success, results):
            if not success:
                _on_error("Falha ao exportar faces da malha (native:exportmeshfaces).")
                return

            # 7.1) Exporta VÉRTICES sincronamente (sem task → sem notificação automática)
            try:
                processing.run(
                    "native:exportmeshvertices",
                    params_verts,
                    context=self._proc_context,
                    feedback=self._proc_feedback)
            except Exception as e:
                _on_error(f"Falha ao exportar vértices da malha (native:exportmeshvertices): {e}")
                return

            # 7.2) Carrega GPKGs e converte para dados Python
            faces_vl = QgsVectorLayer(faces_path, "faces_tmp", "ogr")
            verts_vl = QgsVectorLayer(verts_path, "verts_tmp", "ogr")
            if not faces_vl.isValid() or not verts_vl.isValid():
                _on_error("Falha ao carregar arquivos temporários gerados (GPKG).")
                return

            try:
                polygons, point_z_values = self._to_python_mesh_data(faces_vl, verts_vl)
            except Exception as e:
                _on_error(f"Falha ao converter dados para Python: {e}")
                return

            # 7.3) Barra determinada; o worker definirá o máximo real
            progressBar.setRange(0, 1)

            # 7.4) Dispara o worker DAE
            self._start_export_worker(fmt="DAE", worker_cls=DaeExportWorker, polygons=polygons, point_z_values=point_z_values, save_path=save_path, progressBar=progressBar, progressMsgBar=progressMsgBar)

            # 7.5) Limpeza final
            _cleanup()

        # 8) Conecta e dispara a task de FACES
        self._task_faces.executed.connect(_on_faces_executed)
        QgsApplication.taskManager().addTask(self._task_faces)

    def _obj_convert_and_export_with_tasks(self, mesh_layer, save_path, progressBar, progressMsgBar):
        """
        OBJ:
        - Cria APENAS a task das FACES (assíncrona, com descrição controlada)
        - Ao concluir faces, exporta VÉRTICES de forma síncrona (processing.run)
          → não aparece a notificação automática do QGIS para os vértices
        - Converte GPKG → Python e inicia ObjExportWorker
        """
        # 1) Barra indeterminada no início
        progressBar.setRange(0, 0)

        # 2) Tempdir guardado em self
        self._obj_tmpdir = tempfile.mkdtemp(prefix="objconv_")
        faces_path = os.path.join(self._obj_tmpdir, "faces.gpkg")
        verts_path = os.path.join(self._obj_tmpdir, "verts.gpkg")

        def _cleanup():
            if getattr(self, "_obj_tmpdir", None):
                try:
                    shutil.rmtree(self._obj_tmpdir)
                except Exception:
                    pass
                self._obj_tmpdir = None
            for attr in ("_obj_context", "_obj_feedback", "_task_faces_obj"):
                if hasattr(self, attr):
                    setattr(self, attr, None)

        def _on_error(msg):
            try:
                self.iface.messageBar().popWidget(wait_item)
            except Exception:
                try: wait_item.close()
                except Exception: pass
            self.mostrar_mensagem(msg, "Erro")
            _cleanup()

        # 3) Parâmetros de exportação
        crs_wkt = mesh_layer.crs().toWkt()
        params_faces = {
            'INPUT': mesh_layer,
            'DATASET_GROUPS': [],
            'DATASET_TIME': {'type': 'static'},
            'CRS_OUTPUT': crs_wkt,
            'VECTOR_OPTION': 0,   # polygons
            'OUTPUT': faces_path}
        params_verts = {
            'INPUT': mesh_layer,
            'DATASET_GROUPS': [],
            'DATASET_TIME': {'type': 'static'},
            'CRS_OUTPUT': crs_wkt,
            'VECTOR_OPTION': 1,   # points
            'OUTPUT': verts_path}

        # 4) Algoritmo para FACES
        reg = QgsApplication.processingRegistry()
        alg_faces = reg.createAlgorithmById("native:exportmeshfaces")
        if not alg_faces:
            _on_error("Algoritmo 'native:exportmeshfaces' não encontrado.")
            return

        # 5) Contexto/feedback fortes
        self._obj_context  = QgsProcessingContext()
        self._obj_feedback = QgsProcessingFeedback()

        # 6) Task SOMENTE das FACES (com descrição desejada)
        self._task_faces_obj = QgsProcessingAlgRunnerTask(alg_faces, params_faces, self._obj_context, self._obj_feedback)
        self._task_faces_obj.setDescription('Exporta malha de faces')

        # 7) Callback das FACES
        def _on_faces_executed(success, results):
            if not success:
                _on_error("Falha ao exportar faces da malha (native:exportmeshfaces).")
                return

            # 7.1) Exportar VÉRTICES sincronamente (sem task → sem notificação automática)
            try:
                processing.run(
                    "native:exportmeshvertices",
                    params_verts,
                    context=self._obj_context,
                    feedback=self._obj_feedback)
            except Exception as e:
                _on_error(f"Falha ao exportar vértices da malha (native:exportmeshvertices): {e}")
                return

            # 7.2) Carrega GPKGs e converte em dados Python
            faces_vl = QgsVectorLayer(faces_path, "faces_tmp", "ogr")
            verts_vl = QgsVectorLayer(verts_path, "verts_tmp", "ogr")
            if not faces_vl.isValid() or not verts_vl.isValid():
                _on_error("Falha ao carregar arquivos temporários gerados (GPKG).")
                return

            try:
                polygons, point_z_values = self._to_python_mesh_data(faces_vl, verts_vl)
            except Exception as e:
                _on_error(f"Falha ao converter dados para Python: {e}")
                return

            # 7.3) Barra determinada; o worker ajustará o máximo real
            progressBar.setRange(0, 1)

            # 7.4) Inicia worker OBJ
            self._start_export_worker(fmt="OBJ", worker_cls=ObjExportWorker, polygons=polygons, point_z_values=point_z_values, save_path=save_path, progressBar=progressBar, progressMsgBar=progressMsgBar)

            # 7.5) Limpeza final
            _cleanup()

        # 8) Conecta e dispara a task de FACES
        self._task_faces_obj.executed.connect(_on_faces_executed)
        QgsApplication.taskManager().addTask(self._task_faces_obj)

    def _stl_convert_and_export_with_tasks(self, mesh_layer, save_path, progressBar, progressMsgBar):
        """
        STL:
        - Cria APENAS a task das FACES (assíncrona, com descrição controlada)
        - Ao concluir faces, exporta VÉRTICES de forma síncrona (processing.run)
          → não aparece a notificação automática do QGIS para os vértices
        - Converte GPKG → Python e inicia StlExportWorker
        """
        # 1) Barra indeterminada
        progressBar.setRange(0, 0)

        # 2) Tempdir guardado em self
        self._stl_tmpdir = tempfile.mkdtemp(prefix="stlconv_")
        faces_path = os.path.join(self._stl_tmpdir, "faces.gpkg")
        verts_path = os.path.join(self._stl_tmpdir, "verts.gpkg")

        def _cleanup():
            if getattr(self, "_stl_tmpdir", None):
                try:
                    shutil.rmtree(self._stl_tmpdir)
                except Exception:
                    pass
                self._stl_tmpdir = None
            for attr in ("_stl_context", "_stl_feedback", "_task_faces_stl"):
                if hasattr(self, attr):
                    setattr(self, attr, None)

        def _on_error(msg):
            try:
                self.iface.messageBar().popWidget(wait_item)
            except Exception:
                try: wait_item.close()
                except Exception: pass
            self.mostrar_mensagem(msg, "Erro")
            _cleanup()

        # 3) Parâmetros
        crs_wkt = mesh_layer.crs().toWkt()
        params_faces = {
            'INPUT': mesh_layer,
            'DATASET_GROUPS': [],
            'DATASET_TIME': {'type': 'static'},
            'CRS_OUTPUT': crs_wkt,
            'VECTOR_OPTION': 0,   # polygons
            'OUTPUT': faces_path}
        params_verts = {
            'INPUT': mesh_layer,
            'DATASET_GROUPS': [],
            'DATASET_TIME': {'type': 'static'},
            'CRS_OUTPUT': crs_wkt,
            'VECTOR_OPTION': 1,   # points
            'OUTPUT': verts_path }

        # 4) Algoritmo (apenas faces como task)
        reg = QgsApplication.processingRegistry()
        alg_faces = reg.createAlgorithmById("native:exportmeshfaces")
        if not alg_faces:
            _on_error("Algoritmo 'native:exportmeshfaces' não encontrado.")
            return

        # 5) Contexto/feedback fortes
        self._stl_context  = QgsProcessingContext()
        self._stl_feedback = QgsProcessingFeedback()

        # 6) Task SOMENTE das FACES (com descrição desejada)
        self._task_faces_stl = QgsProcessingAlgRunnerTask(alg_faces, params_faces, self._stl_context, self._stl_feedback)
        self._task_faces_stl.setDescription('Exporta malha de faces')

        # 7) Callback das faces
        def _on_faces_executed(success, results):
            if not success:
                _on_error("Falha ao exportar faces da malha (native:exportmeshfaces).")
                return

            # 7.1) Exporta VÉRTICES sincronamente (sem task → sem notificação automática)
            try:
                processing.run(
                    "native:exportmeshvertices",
                    params_verts,
                    context=self._stl_context,
                    feedback=self._stl_feedback)
            except Exception as e:
                _on_error(f"Falha ao exportar vértices da malha (native:exportmeshvertices): {e}")
                return

            # 7.2) Carrega GPKGs e converte para dados Python
            faces_vl = QgsVectorLayer(faces_path, "faces_tmp", "ogr")
            verts_vl = QgsVectorLayer(verts_path, "verts_tmp", "ogr")
            if not faces_vl.isValid() or not verts_vl.isValid():
                _on_error("Falha ao carregar arquivos temporários gerados (GPKG).")
                return

            try:
                polygons, point_z_values = self._to_python_mesh_data(faces_vl, verts_vl)
            except Exception as e:
                _on_error(f"Falha ao converter dados para Python: {e}")
                return

            # 7.3) Barra determinada; o worker ajustará o máximo real
            progressBar.setRange(0, 1)

            # 7.4) Inicia worker STL (mantendo binary=True)
            self._start_export_worker(fmt="STL", worker_cls=StlExportWorker, polygons=polygons, point_z_values=point_z_values, save_path=save_path, progressBar=progressBar, progressMsgBar=progressMsgBar, binary=True)

            # 7.5) Limpeza final
            _cleanup()

        # 8) Conecta e dispara a task de FACES
        self._task_faces_stl.executed.connect(_on_faces_executed)
        QgsApplication.taskManager().addTask(self._task_faces_stl)

class PrepTask(QgsTask):
    """
    Prepara dados para visualização SEM travar a UI:
      - Converte a QgsMeshLayer em (polygons, point_z_values) prontos para triangulação.
    Requisitos:
      - convert_mesh_to_polygons_and_points(layer) NÃO deve mexer em UI.
    """
    finished_ok = pyqtSignal(list, dict)  # polygons, point_z_values
    failed = pyqtSignal(str)

    def __init__(self, ui_manager, layer_id: str):
        super().__init__("Preparando malha para visualização", QgsTask.CanCancel)
        self._ui = ui_manager
        self._layer_id = layer_id
        self._err = None
        self._polygons = None
        self._point_z = None

    def run(self):
        try:
            layer = QgsProject.instance().mapLayer(self._layer_id)
            if layer is None or not isinstance(layer, QgsMeshLayer):
                raise RuntimeError("Camada inválida durante a preparação.")

            # Passo 1: converter malha (não tocar UI aqui!)
            # Esperado: (QgsVectorLayer de faces, dict {(x,y): z})
            poly_layer, point_z_values = self._ui.convert_mesh_to_polygons_and_points(layer)

            # Passo 2: extrair polígonos (coordenadas) para Python puro
            polygons = self._ui._polygons_from_vector_layer(poly_layer)

            # Resultado para o callback
            self._polygons = polygons
            self._point_z = point_z_values
            return True

        except Exception as e:
            self._err = str(e)
            return False

    def finished(self, result):
        # roda no main thread
        if result and self._err is None:
            try:
                self.finished_ok.emit(self._polygons or [], self._point_z or {})
            except Exception as e:
                self.failed.emit(str(e))
        else:
            self.failed.emit(self._err or "Falha desconhecida na preparação.")

    def cancel(self):
        super().cancel()

class BuffersWorker(QObject):
    max_value  = pyqtSignal(int)
    progress   = pyqtSignal(int)
    finished   = pyqtSignal(list, list, float)  # positions(float flat list), tri_indices(int flat list), duration
    error      = pyqtSignal(str)

    def __init__(self, polygons, point_z_values, eps: float = 1e-9):
        super().__init__()
        # normaliza chaves para (x,y) com 9 casas p/ consistência com export workers
        self.polygons = polygons
        self.point_z_values = {(round(x,9), round(y,9)): float(z) for (x,y), z in point_z_values.items()}
        self.eps = float(eps)
        self._cancel = False

    def cancel(self):
        self._cancel = True

    @staticmethod
    def _area2(a, b, c):
        return (b[0] - a[0])*(c[1] - a[1]) - (b[1] - a[1])*(c[0] - a[0])

    def run(self):
        try:
            t0 = time.time()
            positions = []  # [x0,y0,z0, x1,y1,z1, ...]
            tri_indices = []  # [i0,i1,i2, i3,i4,i5, ...]
            indices = {}   # (x,y) -> idx
            progress = 0

            # estima máximo de faces (fan por anel)
            total_faces_est = 0
            prepared_polys = []
            for poly in self.polygons:  # poly = [ring0, ring1, ...]
                new_poly = []
                for ring in poly:
                    if not ring:
                        continue
                    # remove último=primeiro, se vier fechado
                    if len(ring) >= 2 and ring[0] == ring[-1]:
                        ring = ring[:-1]
                    new_poly.append(ring)
                    n = len(ring)
                    if n >= 3:
                        total_faces_est += (n - 2)
                prepared_polys.append(new_poly)

            self.max_value.emit(max(1, total_faces_est))

            def ensure_index(xy):
                if xy not in indices:
                    z = self.point_z_values.get((round(xy[0],9), round(xy[1],9)), 0.0)
                    idx = len(positions)//3
                    positions.extend([xy[0], xy[1], z])
                    indices[xy] = idx
                return indices[xy]

            for poly in prepared_polys:
                for ring in poly:
                    n = len(ring)
                    if n < 3:
                        continue
                    p0 = ring[0]
                    for i in range(1, n - 1):
                        if self._cancel:
                            raise RuntimeError("Operação cancelada pelo usuário.")
                        p1 = ring[i]
                        p2 = ring[i+1]
                        if abs(self._area2(p0, p1, p2)) < self.eps:
                            continue
                        i0 = ensure_index((round(p0[0],9), round(p0[1],9)))
                        i1 = ensure_index((round(p1[0],9), round(p1[1],9)))
                        i2 = ensure_index((round(p2[0],9), round(p2[1],9)))
                        tri_indices.extend([i0, i1, i2])

                        progress += 1
                        if progress % 100 == 0:
                            self.progress.emit(progress)

            self.max_value.emit(max(1, progress))
            self.progress.emit(progress)
            self.finished.emit(positions, tri_indices, time.time() - t0)

        except Exception as e:
            self.error.emit(str(e))

class DaeExportWorker(QObject):
    max_value  = pyqtSignal(int)
    progress   = pyqtSignal(int)
    finished   = pyqtSignal(str, float)  # path, duration
    error      = pyqtSignal(str)

    def __init__(self, polygons, point_z_values, output_path: str, color_map: dict | None = None):
        super().__init__()
        self.polygons = polygons
        self.point_z_values = {(round(x,9), round(y,9)): z for (x,y), z in point_z_values.items()}
        self.output_path = output_path
        self.color_map = {float(k): tuple(v) for k, v in (color_map or {}).items()}
        self._cancel = False

    def cancel(self):
        self._cancel = True

    @staticmethod
    def _area2(p1, p2, p3):
        return (p2[0]-p1[0])*(p3[1]-p1[1]) - (p3[0]-p1[0])*(p2[1]-p1[1])

    @staticmethod
    def _open_ring(ring):
        """Remove o último ponto se o anel estiver fechado (repete o primeiro)."""
        if not ring:
            return ring
        x0, y0 = ring[0]
        x1, y1 = ring[-1]
        if abs(x0 - x1) < 1e-12 and abs(y0 - y1) < 1e-12:
            return ring[:-1]
        return ring

    def run(self):
        try:
            t0 = time.time()

            positions, colors, indices = [], [], {}
            zs = sorted(set(self.point_z_values.values()))
            z_min, z_max = (zs[0], zs[-1]) if zs else (0.0, 1.0)

            def lerp(a, b, t): return a + (b - a) * t

            def color_for_z(z):
                if self.color_map:
                    # exato ou aproximação pelo Z mais próximo
                    if z in self.color_map:
                        return self.color_map[z]
                    if zs:
                        nearest = min(zs, key=lambda v: abs(v - z))
                        return self.color_map.get(nearest, (1.0, 1.0, 1.0, 1.0))
                    return (1.0, 1.0, 1.0, 1.0)
                # fallback: azul→vermelho
                t = 0.0 if z_max == z_min else (z - z_min)/(z_max - z_min)
                return (lerp(0.0, 1.0, t), 0.0, lerp(1.0, 0.0, t), 1.0)

            def ensure_index(xy):
                if xy not in indices:
                    z = self.point_z_values.get((round(xy[0],9), round(xy[1],9)), 0.0)
                    idx = len(positions)//3
                    positions.extend([xy[0], xy[1], z])
                    colors.extend(color_for_z(z))
                    indices[xy] = idx
                return indices[xy]

            # Preparar anéis abertos e estimar faces
            prepared_polys = []
            total_faces_est = 0
            for poly in self.polygons:
                new_poly = []
                for ring in poly:
                    ring2 = self._open_ring(ring)  # <<< usar método da classe
                    n = len(ring2)
                    if n >= 3:
                        total_faces_est += (n - 2)
                    new_poly.append(ring2)
                prepared_polys.append(new_poly)

            self.max_value.emit(total_faces_est if total_faces_est > 0 else 1)

            # Triangulação tipo "fan" + progresso 100/100
            tri_indices = []
            progress = 0
            eps = 1e-12
            for poly in prepared_polys:
                for ring in poly:
                    n = len(ring)
                    if n < 3:
                        continue
                    p0 = ring[0]
                    for i in range(1, n - 1):
                        if self._cancel:
                            raise RuntimeError("Operação cancelada pelo usuário.")
                        p1 = ring[i]
                        p2 = ring[i+1]
                        if abs(self._area2(p0, p1, p2)) < eps:
                            continue
                        i0 = ensure_index((round(p0[0],9), round(p0[1],9)))
                        i1 = ensure_index((round(p1[0],9), round(p1[1],9)))
                        i2 = ensure_index((round(p2[0],9), round(p2[1],9)))
                        tri_indices.extend([i0, i1, i2])
                        progress += 1
                        if progress % 100 == 0:
                            self.progress.emit(progress)

            self.max_value.emit(max(1, progress))
            self.progress.emit(progress)

            # Escrever COLLADA (igual ao que você já tinha)
            collada = ET.Element("COLLADA", xmlns="http://www.collada.org/2005/11/COLLADASchema", version="1.4.1")
            asset = ET.SubElement(collada, "asset")
            ET.SubElement(asset, "created").text  = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
            ET.SubElement(asset, "modified").text = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
            ET.SubElement(asset, "unit", name="meter", meter="1")
            ET.SubElement(asset, "up_axis").text  = "Z_UP"

            lib_geo  = ET.SubElement(collada, "library_geometries")
            geometry = ET.SubElement(lib_geo, "geometry", id="mesh", name="mesh")
            mesh     = ET.SubElement(geometry, "mesh")

            def _append_source(mesh_elem, name, stride, array):
                src = ET.SubElement(mesh_elem, "source", id=f"mesh-{name}")
                fa  = ET.SubElement(src, "float_array", id=f"mesh-{name}-array", count=str(len(array)))
                fa.text = " ".join(map(str, array))
                tech = ET.SubElement(src, "technique_common")
                acc  = ET.SubElement(tech, "accessor", source=f"#mesh-{name}-array",
                                     count=str(len(array)//stride), stride=str(stride))
                if name == "positions":
                    ET.SubElement(acc, "param", name="X", type="float")
                    ET.SubElement(acc, "param", name="Y", type="float")
                    ET.SubElement(acc, "param", name="Z", type="float")
                elif name == "colors":
                    for n in ("R","G","B","A"):
                        ET.SubElement(acc, "param", name=n, type="float")
                return src

            _append_source(mesh, "positions", 3, positions)
            _append_source(mesh, "colors",    4, colors)

            verts = ET.SubElement(mesh, "vertices", id="mesh-vertices")
            ET.SubElement(verts, "input", semantic="POSITION", source="#mesh-positions")
            ET.SubElement(verts, "input", semantic="COLOR",    source="#mesh-colors")

            real_count = len(tri_indices) // 3
            tris = ET.SubElement(mesh, "triangles", count=str(real_count))
            ET.SubElement(tris, "input", semantic="VERTEX", source="#mesh-vertices", offset="0")
            p_elem = ET.SubElement(tris, "p")
            p_elem.text = " ".join(map(str, tri_indices))

            lib_scenes = ET.SubElement(collada, "library_visual_scenes")
            v_scene = ET.SubElement(lib_scenes, "visual_scene", id="Scene", name="Scene")
            node = ET.SubElement(v_scene, "node", id="mesh", name="mesh", type="NODE")
            ET.SubElement(node, "matrix", sid="transform").text = "1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1"
            ET.SubElement(node, "instance_geometry", url="#mesh")
            scene = ET.SubElement(collada, "scene")
            ET.SubElement(scene, "instance_visual_scene", url="#Scene")

            ET.ElementTree(collada).write(self.output_path, encoding="UTF-8", xml_declaration=True)
            self.finished.emit(self.output_path, time.time() - t0)

        except Exception as e:
            self.error.emit(str(e))

class KmlExportWorker(QObject):
    max_value  = pyqtSignal(int)
    progress   = pyqtSignal(int)
    finished   = pyqtSignal(str, float)  # path, duration
    error      = pyqtSignal(str)

    def __init__(self, polygons, point_z_values_unused, output_path: str, style: dict):
        super().__init__()
        # polygons: List[List[List[Tuple[lon,lat,z]]]]
        self.polygons = polygons
        self.output_path = output_path
        self.style = style
        self._cancel = False

    def cancel(self): self._cancel = True

    @staticmethod
    def _ensure_closed(ring3d):
        if not ring3d: return ring3d
        a = ring3d[0]; b = ring3d[-1]
        if abs(a[0]-b[0])<1e-12 and abs(a[1]-b[1])<1e-12 and abs(a[2]-b[2])<1e-12:
            return ring3d
        return ring3d + [ring3d[0]]

    def run(self):
        try:
            import time
            t0 = time.time()

            total = len(self.polygons)
            self.max_value.emit(total if total>0 else 1)

            kml = ET.Element("kml", xmlns="http://www.opengis.net/kml/2.2")
            doc = ET.SubElement(kml, "Document")

            # Estilo único
            style = ET.SubElement(doc, "Style", id=self.style.get("style_id","customStyle"))
            ls = ET.SubElement(style, "LineStyle")
            ET.SubElement(ls, "color").text = self.style["line_color_abgr"]
            ET.SubElement(ls, "width").text = str(self.style["line_width"])
            ps = ET.SubElement(style, "PolyStyle")
            ET.SubElement(ps, "color").text = self.style["poly_color_abgr"]
            ET.SubElement(ps, "fill").text = "1" if self.style.get("fill",1) else "0"
            ET.SubElement(ps, "outline").text = "1" if self.style.get("outline",1) else "0"

            progress = 0

            for poly in self.polygons:
                if self._cancel:
                    raise RuntimeError("Operação cancelada pelo usuário.")

                placemark = ET.SubElement(doc, "Placemark")
                ET.SubElement(placemark, "styleUrl").text = f"#{self.style.get('style_id','customStyle')}"
                polygon_elem = ET.SubElement(placemark, "Polygon")
                ET.SubElement(polygon_elem, "altitudeMode").text = "absolute"

                if len(poly) >= 1:
                    # outer
                    outer = ET.SubElement(polygon_elem, "outerBoundaryIs")
                    lr = ET.SubElement(outer, "LinearRing")
                    ring = self._ensure_closed(poly[0])
                    coords = " ".join([f"{x},{y},{z}" for (x,y,z) in ring])
                    ET.SubElement(lr, "coordinates").text = coords

                # holes (inners)
                if len(poly) > 1:
                    for ring3d in poly[1:]:
                        inner = ET.SubElement(polygon_elem, "innerBoundaryIs")
                        lr = ET.SubElement(inner, "LinearRing")
                        ring = self._ensure_closed(ring3d)
                        coords = " ".join([f"{x},{y},{z}" for (x,y,z) in ring])
                        ET.SubElement(lr, "coordinates").text = coords

                progress += 1
                if progress % 100 == 0:
                    self.progress.emit(progress)

            # Finaliza progresso e grava
            self.max_value.emit(max(1, progress))
            self.progress.emit(progress)

            ET.ElementTree(kml).write(self.output_path, xml_declaration=True, encoding="utf-8")
            self.finished.emit(self.output_path, time.time() - t0)

        except Exception as e:
            self.error.emit(str(e))

class StlExportWorker(QObject):
    max_value  = pyqtSignal(int)
    progress   = pyqtSignal(int)
    finished   = pyqtSignal(str, float)  # path, duration
    error      = pyqtSignal(str)

    def __init__(self, polygons, point_z_values, output_path: str, binary: bool = True):
        super().__init__()
        # polygons: List[List[List[Tuple[float,float]]]] (polígono→anéis→(x,y))
        self.polygons = polygons
        # normaliza para reduzir problemas de flutuante
        self.point_z_values = {(round(x,9), round(y,9)): z for (x,y), z in point_z_values.items()}
        self.output_path = output_path
        self.binary = binary
        self._cancel = False

    def cancel(self):
        self._cancel = True

    @staticmethod
    def _open_ring(ring):
        if not ring:
            return ring
        x0, y0 = ring[0]
        x1, y1 = ring[-1]
        if abs(x0 - x1) < 1e-12 and abs(y0 - y1) < 1e-12:
            return ring[:-1]
        return ring

    @staticmethod
    def _area2(p1, p2, p3):
        return (p2[0]-p1[0])*(p3[1]-p1[1]) - (p3[0]-p1[0])*(p2[1]-p1[1])

    @staticmethod
    def _normal(a, b, c):
        # normal de (a,b,c) com produto vetorial, normalizada
        ux, uy, uz = b[0]-a[0], b[1]-a[1], b[2]-a[2]
        vx, vy, vz = c[0]-a[0], c[1]-a[1], c[2]-a[2]
        nx = uy*vz - uz*vy
        ny = uz*vx - ux*vz
        nz = ux*vy - uy*vx
        norm = (nx*nx + ny*ny + nz*nz) ** 0.5
        if norm == 0.0:
            return (0.0, 0.0, 0.0)
        return (nx/norm, ny/norm, nz/norm)

    def run(self):
        try:
            import time, struct
            t0 = time.time()

            # 1) Abrir anéis e estimar faces (para setar um "max" inicial)
            prepared_polys = []
            total_faces_est = 0
            for poly in self.polygons:
                new_poly = []
                for ring in poly:
                    ring2 = self._open_ring(ring)
                    n = len(ring2)
                    if n >= 3:
                        total_faces_est += (n - 2)
                    new_poly.append(ring2)
                prepared_polys.append(new_poly)

            self.max_value.emit(total_faces_est if total_faces_est > 0 else 1)

            def z_of(xy):
                return self.point_z_values.get((round(xy[0],9), round(xy[1],9)), 0.0)

            progress = 0
            real_count = 0
            eps = 1e-12

            if self.binary:
                # STL BINÁRIO
                # Formato:
                # 80 bytes header
                # 4 bytes (uint32) count
                # N * (normal[3]*float32 + v1[3] + v2[3] + v3[3] + uint16 attribute)
                with open(self.output_path, "wb") as f:
                    header = b"STL binary generated by plugin".ljust(80, b" ")
                    f.write(header)
                    f.write(struct.pack("<I", 0))  # placeholder para count (corrigiremos após escrever)

                    pack_tri = struct.Struct("<12fH").pack  # normal(3) + 3*vertex(3) + attr(0)
                    for poly in prepared_polys:
                        for ring in poly:
                            n = len(ring)
                            if n < 3:
                                continue
                            p0 = ring[0]
                            for i in range(1, n - 1):
                                if self._cancel:
                                    raise RuntimeError("Operação cancelada pelo usuário.")
                                p1 = ring[i]
                                p2 = ring[i+1]
                                if abs(self._area2(p0, p1, p2)) < eps:
                                    continue

                                a = (p0[0], p0[1], z_of(p0))
                                b = (p1[0], p1[1], z_of(p1))
                                c = (p2[0], p2[1], z_of(p2))
                                nx, ny, nz = self._normal(a, b, c)

                                # empacota e escreve triângulo
                                f.write(pack_tri(
                                    float(nx), float(ny), float(nz),
                                    float(a[0]), float(a[1]), float(a[2]),
                                    float(b[0]), float(b[1]), float(b[2]),
                                    float(c[0]), float(c[1]), float(c[2]),
                                    0  # attribute byte count
                                ))

                                real_count += 1
                                progress += 1
                                if progress % 100 == 0:
                                    self.progress.emit(progress)

                    # Corrige a contagem real no cabeçalho
                    f.seek(80)
                    f.write(struct.pack("<I", real_count))

                # Ajuste final da barra
                self.max_value.emit(max(1, progress))
                self.progress.emit(progress)

            else:
                # STL ASCII
                with open(self.output_path, "w", encoding="utf-8") as f:
                    f.write("solid mesh\n")
                    for poly in prepared_polys:
                        for ring in poly:
                            n = len(ring)
                            if n < 3:
                                continue
                            p0 = ring[0]
                            for i in range(1, n - 1):
                                if self._cancel:
                                    raise RuntimeError("Operação cancelada pelo usuário.")
                                p1 = ring[i]
                                p2 = ring[i+1]
                                if abs(self._area2(p0, p1, p2)) < eps:
                                    continue

                                a = (p0[0], p0[1], z_of(p0))
                                b = (p1[0], p1[1], z_of(p1))
                                c = (p2[0], p2[1], z_of(p2))
                                nx, ny, nz = self._normal(a, b, c)

                                f.write(f"facet normal {nx} {ny} {nz}\n")
                                f.write("  outer loop\n")
                                f.write(f"    vertex {a[0]} {a[1]} {a[2]}\n")
                                f.write(f"    vertex {b[0]} {b[1]} {b[2]}\n")
                                f.write(f"    vertex {c[0]} {c[1]} {c[2]}\n")
                                f.write("  endloop\n")
                                f.write("endfacet\n")

                                real_count += 1
                                progress += 1
                                if progress % 100 == 0:
                                    self.progress.emit(progress)

                    self.max_value.emit(max(1, progress))
                    self.progress.emit(progress)
                    f.write("endsolid mesh\n")

            self.finished.emit(self.output_path, time.time() - t0)

        except Exception as e:
            self.error.emit(str(e))

class ObjExportWorker(QObject):
    max_value  = pyqtSignal(int)
    progress   = pyqtSignal(int)
    finished   = pyqtSignal(str, float)  # path, duration
    error      = pyqtSignal(str)

    def __init__(self, polygons, point_z_values, output_path: str, dedup_vertices: bool = True):
        super().__init__()
        # polygons: List[List[List[Tuple[float,float]]]]  (polígono→anéis→(x,y))
        self.polygons = polygons
        self.point_z_values = {(round(x,9), round(y,9)): z for (x,y), z in point_z_values.items()}
        self.output_path = output_path
        self.dedup_vertices = dedup_vertices
        self._cancel = False

    def cancel(self):
        self._cancel = True

    @staticmethod
    def _area2(p1, p2, p3):
        return (p2[0]-p1[0])*(p3[1]-p1[1]) - (p3[0]-p1[0])*(p2[1]-p1[1])

    @staticmethod
    def _open_ring(ring):
        if not ring:
            return ring
        x0, y0 = ring[0]
        x1, y1 = ring[-1]
        if abs(x0 - x1) < 1e-12 and abs(y0 - y1) < 1e-12:
            return ring[:-1]
        return ring

    def run(self):
        try:
            import time
            t0 = time.time()

            # 1) Preparar anéis abertos e estimar faces
            prepared_polys = []
            total_faces_est = 0
            for poly in self.polygons:
                new_poly = []
                for ring in poly:
                    ring2 = self._open_ring(ring)
                    n = len(ring2)
                    if n >= 3:
                        total_faces_est += (n - 2)
                    new_poly.append(ring2)
                prepared_polys.append(new_poly)

            self.max_value.emit(total_faces_est if total_faces_est > 0 else 1)

            # 2) Tabelas para OBJ (v e f)
            positions = []               # [x0,y0,z0, x1,y1,z1, ...]
            vertex_index = {}            # (x,y,z) -> idx (1-based para OBJ)
            faces_idx = []               # [ (i0,i1,i2), ... ]
            progress = 0
            eps = 1e-12

            def z_of(xy):
                return self.point_z_values.get((round(xy[0],9), round(xy[1],9)), 0.0)

            def ensure_vertex(xy):
                z = z_of(xy)
                key = (round(xy[0],9), round(xy[1],9), z if self.dedup_vertices else time.time())
                idx = vertex_index.get(key)
                if idx is None:
                    idx = len(positions)//3 + 1  # OBJ é 1-based
                    positions.extend([xy[0], xy[1], z])
                    vertex_index[key] = idx
                return idx

            # 3) Triangulação fan (igual aos outros workers)
            for poly in prepared_polys:
                for ring in poly:
                    n = len(ring)
                    if n < 3:
                        continue
                    p0 = ring[0]
                    for i in range(1, n - 1):
                        if self._cancel:
                            raise RuntimeError("Operação cancelada pelo usuário.")
                        p1 = ring[i]
                        p2 = ring[i+1]
                        if abs(self._area2(p0, p1, p2)) < eps:
                            continue

                        i0 = ensure_vertex((p0[0], p0[1]))
                        i1 = ensure_vertex((p1[0], p1[1]))
                        i2 = ensure_vertex((p2[0], p2[1]))
                        faces_idx.append((i0, i1, i2))

                        progress += 1
                        if progress % 100 == 0:
                            self.progress.emit(progress)

            # 4) Ajuste final da barra e gravação do arquivo
            self.max_value.emit(max(1, progress))
            self.progress.emit(progress)

            # Escrever OBJ
            lines = []
            lines.append("# OBJ exportado por plugin (triangulado)")
            # v x y z
            for i in range(0, len(positions), 3):
                lines.append(f"v {positions[i]} {positions[i+1]} {positions[i+2]}")
            # faces triangulares
            # (sem normais/texcoords; pode-se adicionar 'vn' depois)
            for (a,b,c) in faces_idx:
                lines.append(f"f {a} {b} {c}")

            with open(self.output_path, "w", encoding="utf-8") as f:
                f.write("\n".join(lines) + "\n")

            self.finished.emit(self.output_path, time.time() - t0)

        except Exception as e:
            self.error.emit(str(e))

class DxfExportWorker(QObject):
    max_value  = pyqtSignal(int)
    progress   = pyqtSignal(int)
    finished   = pyqtSignal(str, float)  # path, duration
    error      = pyqtSignal(str)

    def __init__(self, polygons, point_z_values, output_path: str, dedup_faces: bool = True):
        super().__init__()
        # polygons: List[List[List[Tuple[float,float]]]]  (polígonos→anéis→(x,y))
        self.polygons = polygons
        # normaliza as chaves (x,y) para bater com floats
        self.point_z_values = {(round(x,9), round(y,9)): z for (x,y), z in point_z_values.items()}
        self.output_path = output_path
        self.dedup_faces = dedup_faces
        self._cancel = False

    def cancel(self):
        self._cancel = True

    @staticmethod
    def _area2(p1, p2, p3):
        return (p2[0]-p1[0])*(p3[1]-p1[1]) - (p3[0]-p1[0])*(p2[1]-p1[1])

    @staticmethod
    def _open_ring(ring):
        if not ring:
            return ring
        x0, y0 = ring[0]
        x1, y1 = ring[-1]
        if abs(x0 - x1) < 1e-12 and abs(y0 - y1) < 1e-12:
            return ring[:-1]
        return ring

    def run(self):
        try:
            import time
            try:
                import ezdxf
            except ImportError:
                self.error.emit("O pacote 'ezdxf' não está instalado no ambiente do QGIS.")
                return

            t0 = time.time()

            # 1) Preparar anéis abertos e estimar faces
            prepared_polys = []
            total_faces_est = 0
            for poly in self.polygons:
                new_poly = []
                for ring in poly:
                    ring2 = self._open_ring(ring)
                    n = len(ring2)
                    if n >= 3:
                        total_faces_est += (n - 2)
                    new_poly.append(ring2)
                prepared_polys.append(new_poly)

            self.max_value.emit(total_faces_est if total_faces_est > 0 else 1)

            # 2) Criar DXF
            doc = ezdxf.new(dxfversion='R2013')
            msp = doc.modelspace()

            faces_seen = set() if self.dedup_faces else None

            def z_of(xy):
                return self.point_z_values.get((round(xy[0],9), round(xy[1],9)), 0.0)

            progress = 0
            eps = 1e-12

            # 3) Triangulação fan simples (igual ao DAE atual)
            for poly in prepared_polys:
                for ring in poly:
                    n = len(ring)
                    if n < 3:
                        continue
                    p0 = ring[0]
                    for i in range(1, n - 1):
                        if self._cancel:
                            raise RuntimeError("Operação cancelada pelo usuário.")
                        p1 = ring[i]
                        p2 = ring[i+1]
                        if abs(self._area2(p0, p1, p2)) < eps:
                            continue

                        # Deduplicação opcional (por XY)
                        if faces_seen is not None:
                            key = frozenset([
                                (round(p0[0],9), round(p0[1],9)),
                                (round(p1[0],9), round(p1[1],9)),
                                (round(p2[0],9), round(p2[1],9)),
                            ])
                            if key in faces_seen:
                                continue
                            faces_seen.add(key)

                        v0 = (p0[0], p0[1], z_of(p0))
                        v1 = (p1[0], p1[1], z_of(p1))
                        v2 = (p2[0], p2[1], z_of(p2))
                        msp.add_3dface([v0, v1, v2, v2])  # 3DFACE triangular

                        progress += 1
                        if progress % 100 == 0:
                            self.progress.emit(progress)

            # 4) Ajuste final da barra e salvar
            self.max_value.emit(max(1, progress))
            self.progress.emit(progress)

            doc.saveas(self.output_path)

            self.finished.emit(self.output_path, time.time() - t0)

        except Exception as e:
            self.error.emit(str(e))

class TreeViewEventFilter(QObject):
    """
    Filtro de eventos personalizado para detectar movimentos do mouse sobre itens em um treeView.

    Esta classe herda de QObject e implementa um filtro de eventos que detecta quando o mouse se move
    sobre itens específicos em um treeView. Quando o mouse se move sobre um item, a classe chama um 
    método no UiManagerM para exibir um tooltip com informações sobre o item.

    Parâmetros:
    - ui_manager: Referência à instância do objeto UiManagerM, que gerencia a interface do usuário.
    """

    def __init__(self, ui_manager):
        """
        Inicializa o filtro de eventos com uma referência ao UiManagerM.

        Parâmetros:
        - ui_manager: Instância do UiManager que será usada para acessar e manipular a interface do usuário.
        """
        super().__init__()  # Inicializa a classe base QObject
        self.ui_manager = ui_manager  # Armazena a referência ao UiManagerM para uso posterior

    def eventFilter(self, obj, event):
        """
        Filtra os eventos de movimentação do mouse sobre o treeView e exibe tooltips quando aplicável.

        Esta função intercepta eventos que ocorrem no treeView especificado. Se o evento for de movimento
        do mouse (QEvent.MouseMove) e o mouse estiver sobre um item válido no treeView, a função chama
        o método 'configurar_tooltip' do UiManager para exibir um tooltip com informações sobre o item.

        Parâmetros:
        - obj: O objeto que está sendo monitorado (neste caso, o viewport do treeView).
        - event: O evento que está sendo filtrado (como QEvent.MouseMove).

        Retorno:
        - bool: O resultado da chamada à função 'eventFilter' da classe base, indicando se o evento foi processado.
        """
        # Verifica se o objeto é o viewport do treeView e se o evento é de movimento do mouse
        if obj == self.ui_manager.dlg.treeViewListaMalha.viewport() and event.type() == QEvent.Type.MouseMove:
            # Obtém o índice do item no treeView sob o cursor do mouse
            index = self.ui_manager.dlg.treeViewListaMalha.indexAt(event.pos())
            if index.isValid():  # Verifica se o índice é válido (se o mouse está sobre um item)
                self.ui_manager.configurar_tooltip(index)  # Chama o método para configurar e exibir o tooltip
        # Retorna o resultado padrão do filtro de eventos
        return super().eventFilter(obj, event)  # Chama a implementação da classe base para continuar o processamento normal

class VisualizadorDAE3D(QDialog):

    def __init__(self, dae_file_path=None, parent=None, ui_manager=None, positions=None, tri_indices=None):
        """
        Inicializa o visualizador 3D de malhas em formato DAE (COLLADA).

        Esta função configura a janela de visualização 3D, incluindo a interface lateral com
        várias opções de configuração, como inverter o gradiente, salvar a visualização em PNG, e alternar a exibição dos eixos XYZ e das linhas dos vértices.

        Parâmetros:
        - dae_file_path (str): O caminho do arquivo DAE que será visualizado.
        - parent (QWidget): O widget pai da janela de visualização (opcional).
        - ui_manager (UiManagerM): Instância da classe UiManagerM para acessar métodos auxiliares, como salvar arquivos e exibir mensagens.

        Atributos:
        - view (GLViewWidget): O widget principal que renderiza a malha 3D.
        - mesh_data (GLMeshItem): Os dados da malha carregados a partir do arquivo DAE.
        - mesh_item (GLMeshItem): O item da malha renderizada.
        - checkbox_xyz (QCheckBox): Checkbox para exibir ou ocultar os eixos XYZ.
        """
        super().__init__(parent)
        self.ui_manager = ui_manager  # Instância de UiManagerM
        self.dae_file_path = dae_file_path # Caminho do arquivo DAE a ser carregado
        # novos buffers em memória (opcionais)
        self._positions = positions
        self._tri_indices = tri_indices

        self.setWindowTitle("Visualização 3D da Malha")

        # Definir a geometria da janela (largura=1400, altura=900)
        self.setGeometry(100, 100, 1400, 900)

        # Ativar botões de minimizar e maximizar
        self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowMinimizeButtonHint | Qt.WindowType.WindowMaximizeButtonHint)

        # Definir a janela como não modal
        self.setWindowModality(Qt.WindowModality.NonModal)

        # Layout principal
        main_layout = QHBoxLayout()
        self.setLayout(main_layout)

        # Visualizador 3D
        self.view = PannableGLViewWidget()
        # self.view.setCameraPosition(distance=200)  # Define a distância inicial da câmera

        # Câmera básica
        try:
            self.view.setCameraPosition(distance=1000)
        except Exception:
            pass

        # Eixos XYZ (opcional)
        try:
            self._axes = gl.GLAxisItem()
            self._axes.setSize(100,100,100)
            self.view.addItem(self._axes)
        except Exception:
            self._axes = None

        self.view.opts['antialias'] = True  # Ativar suavização para melhor renderização
        main_layout.addWidget(self.view)  # Adicionar o visualizador 3D ao layout principal

        # Frame lateral para botões
        frame = QFrame()
        frame.setFrameShape(QFrame.Shape.Box)
        frame.setFrameShadow(QFrame.Shadow.Raised)
        frame.setFixedWidth(200)
        
        # Layout para os widgets dentro do frame
        side_layout = QVBoxLayout()
        frame.setLayout(side_layout)

        # Fixar o texto "Configurações:" no topo
        label = QLabel("Configurações:")
        label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
        side_layout.addWidget(label, alignment=Qt.AlignmentFlag.AlignTop)

        # Adicionar um espaçador vertical para garantir que o texto e o slider fiquem juntos
        vertical_spacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
        side_layout.addItem(vertical_spacer)

        # Grupo: Escala de Cores (acima do "Salvar como PNG")
        color_frame = QFrame()
        color_frame.setFrameShape(QFrame.Shape.Box)
        color_frame.setFrameShadow(QFrame.Shadow.Raised)
        color_frame.setLineWidth(1)

        color_layout = QVBoxLayout(color_frame)
        color_layout.setContentsMargins(8, 8, 8, 8)
        color_layout.setSpacing(6)

        lbl_color = QLabel("Escala de Cores:")
        lbl_color.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
        # opcional: destaque leve do título
        lbl_color.setStyleSheet("font-weight: 600;")

        self.colorbar = ColorBarWidget()
        self.colorbar.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)

        color_layout.addWidget(lbl_color)
        color_layout.addWidget(self.colorbar)

        side_layout.addWidget(color_frame)

        # Botão para salvar a visualização como PNG
        self.botao_salvar_png = QPushButton("Salvar como PNG")
        self.botao_salvar_png.clicked.connect(self.salvar_visualizacao_como_png)
        side_layout.addWidget(self.botao_salvar_png)

        # Linha Horizontal (acima do texto "Número de faixas de cores:")
        hline_above_slider = QFrame()
        hline_above_slider.setFrameShape(QFrame.Shape.HLine)
        hline_above_slider.setFrameShadow(QFrame.Shadow.Sunken)
        side_layout.addWidget(hline_above_slider)

        # Criar o layout em grade (grid layout)
        grid_layout = QGridLayout()

        # Adicionar o texto "Número de faixas de cores" na primeira linha, coluna 0
        label_faixas = QLabel("Número de faixas de cores:")
        grid_layout.addWidget(label_faixas, 0, 0)

         # Adicionar um QSpinBox para selecionar o número de faixas de cores
        self.spinbox_faixas = QSpinBox()
        self.spinbox_faixas.setMinimum(1)
        self.spinbox_faixas.setMaximum(10)
        self.spinbox_faixas.setValue(10)  # Valor padrão
        self.spinbox_faixas.valueChanged.connect(self.atualizar_malha_com_faixas)
        grid_layout.addWidget(self.spinbox_faixas, 1, 0)

        # Crie um novo layout horizontal para os botões e o slider
        h_layout = QHBoxLayout()

        # Botão para diminuir o valor
        self.button_decrease = QPushButton()
        self.button_decrease.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_ArrowLeft))
        self.button_decrease.setFixedSize(QSize(20, 20))
        self.button_decrease.clicked.connect(self.decrease_slider_value)
        h_layout.addWidget(self.button_decrease)

        # Adicione o slider ao layout horizontal
        self.slider_faixas = QSlider(Qt.Orientation.Horizontal)
        self.slider_faixas.setMinimum(1)
        self.slider_faixas.setMaximum(10)
        self.slider_faixas.setValue(10)
        self.slider_faixas.setTickPosition(QSlider.TickPosition.TicksAbove)
        self.slider_faixas.setTickInterval(1)
        self.slider_faixas.valueChanged.connect(self.atualizar_malha_com_faixas)
        h_layout.addWidget(self.slider_faixas)

        # Mantém sempre sincronizados
        self.slider_faixas.valueChanged.connect(self.spinbox_faixas.setValue)
        self.spinbox_faixas.valueChanged.connect(self.slider_faixas.setValue)

        # Botão para aumentar o valor
        self.button_increase = QPushButton()
        self.button_increase.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_ArrowRight))
        self.button_increase.setFixedSize(QSize(20, 20))
        self.button_increase.clicked.connect(self.increase_slider_value)
        h_layout.addWidget(self.button_increase)

        # Adicione o layout horizontal ao grid_layout
        grid_layout.addLayout(h_layout, 1, 0)

        # Adicionar o layout em grid ao layout principal (side_layout)
        side_layout.addLayout(grid_layout)

        # CheckBox para inverter as cores do gradiente
        self.checkbox_inverter_cores = QCheckBox("Inverter Cores do Gradiente")
        self.checkbox_inverter_cores.setChecked(False)  # Desativado por padrão
        self.checkbox_inverter_cores.stateChanged.connect(self.atualizar_malha_com_faixas)
        side_layout.addWidget(self.checkbox_inverter_cores)

        # Linha Horizontal (abaixo do slider)
        hline_below_slider = QFrame()
        hline_below_slider.setFrameShape(QFrame.Shape.HLine)
        hline_below_slider.setFrameShadow(QFrame.Shadow.Sunken)
        side_layout.addWidget(hline_below_slider)

        # RadioButton para mudar a cor do fundo
        self.radio_white = QRadioButton("Fundo Branco")
        self.radio_black = QRadioButton("Fundo Preto")

        # Definir o fundo branco como padrão
        self.radio_white.setChecked(True)
        self.view.setBackgroundColor('w')

        # Conectar os RadioButtons às funções para alterar a cor do fundo
        self.radio_white.toggled.connect(self.mudar_fundo)
        self.radio_black.toggled.connect(self.mudar_fundo)

        # Adicionar RadioButtons ao layout do frame
        side_layout.addWidget(self.radio_white)
        side_layout.addWidget(self.radio_black)

        # **Adicionar o CheckBox para exibir/ocultar as linhas dos vértices**
        self.checkbox_edges = QCheckBox("Exibir Linhas dos Vértices")
        self.checkbox_edges.setChecked(True)  # Exibir as linhas por padrão
        self.checkbox_edges.stateChanged.connect(self.alternar_linhas_vertices)
        side_layout.addWidget(self.checkbox_edges)

        # Adicionando o checkbox para exibir ou ocultar eixos XYZ
        self.checkbox_xyz = QCheckBox("Exibir Eixos XYZ")
        self.checkbox_xyz.setChecked(True)  # Exibir os eixos por padrão
        self.checkbox_xyz.stateChanged.connect(self.alternar_eixos_xyz)
        side_layout.addWidget(self.checkbox_xyz)

        # Separador abaixo do checkbox XYZ
        hline_after_xyz = QFrame()
        hline_after_xyz.setFrameShape(QFrame.Shape.HLine)
        hline_after_xyz.setFrameShadow(QFrame.Shadow.Sunken)
        side_layout.addWidget(hline_after_xyz)

        # "Ver Tudo: botão" (texto + botão em uma linha)
        ver_tudo_row = QHBoxLayout()
        ver_tudo_label = QLabel("Centralizar Malha:")
        self.btn_ver_tudo = QPushButton("Aperte")
        self.btn_ver_tudo.clicked.connect(self.ver_tudo)  # chama o slot para enquadrar a malha
        ver_tudo_row.addWidget(ver_tudo_label)
        ver_tudo_row.addWidget(self.btn_ver_tudo, 1)
        side_layout.addLayout(ver_tudo_row)

        # Adiciona o frame ao layout principal
        main_layout.addWidget(frame)

        # Variável para armazenar os dados da malha
        self.mesh_data = None  # Inicializa a variável mesh_data como None
        self.mesh_item = None  # Inicializa a variável mesh_item como None

        # Carregar via buffers (se vieram), senão via DAE
        try:
            if self._positions is not None and self._tri_indices is not None:
                verts_np = np.asarray(self._positions, dtype=float).reshape(-1, 3)
                faces_np = np.asarray(self._tri_indices, dtype=int).reshape(-1, 3)
                self.load_and_draw_buffers(verts_np, faces_np)
            else:
                self.load_and_draw_dae()
        except Exception as e:
            if self.ui_manager:
                self.ui_manager.mostrar_mensagem(f"Erro ao carregar malha no visualizador: {e}", "Erro")

        self.eixos_xyz = None  # evita AttributeError e facilita checagens

        # Desenhar os eixos XYZ diretamente na cena
        self.adicionar_eixos_xyz() # Adiciona os eixos XYZ à cena 3D

    def _find_colorbar_widget(self):
        """
        Tenta localizar o widget de colorbar por nomes comuns.
        Deve expor o método update_discrete(labels, band_colors).
        """
        for name in ("colorBar", "colorbarWidget", "widgetColorBar", "colorbar", "cbWidget"):
            cb = getattr(self, name, None)
            if cb is not None and hasattr(cb, "update_discrete"):
                return cb
        return None

    def _update_colorbar_from_arrays(self, z_values: "np.ndarray", colors: "np.ndarray", num_faixas: int):
        """
        Gera faixas [low, high] e uma cor representativa por faixa (média das
        cores dos vértices dentro daquela faixa) e envia para o ColorBarWidget.
        Robusto para z constante, faixas vazias, etc.
        """
        cb = self._find_colorbar_widget()
        if cb is None:
            return

        z_values = np.asarray(z_values).astype(float)
        colors   = np.asarray(colors).astype(float)

        # Sem dados: placeholder único
        if z_values.size == 0 or not np.isfinite(z_values).any():
            cb.update_discrete([""], [(0.85, 0.85, 0.85, 1.0)])
            return

        zmin = float(np.nanmin(z_values))
        zmax = float(np.nanmax(z_values))

        # z constante → uma faixa só
        if not math.isfinite(zmin) or not math.isfinite(zmax) or abs(zmax - zmin) < 1e-12:
            label = f"{zmin:.2f}"
            # usa média global das cores como representativa
            if colors.size:
                c = colors.reshape(-1, colors.shape[-1]).mean(axis=0)
                c = tuple(float(x) for x in c.tolist())
            else:
                c = (0.85, 0.85, 0.85, 1.0)
            cb.update_discrete([label], [c])
            return

        # Constrói edges e agrega cor por banda
        num_faixas = max(1, int(num_faixas))
        edges = np.linspace(zmin, zmax, num_faixas + 1)
        labels = []
        band_colors = []

        flat_colors = colors.reshape(-1, colors.shape[-1]) if colors.ndim > 1 else colors
        for i in range(num_faixas):
            low, high = edges[i], edges[i + 1]
            # último intervalo inclui o high
            if i < num_faixas - 1:
                mask = (z_values >= low) & (z_values < high)
            else:
                mask = (z_values >= low) & (z_values <= high)

            labels.append(f"{low:.2f} – {high:.2f}")

            if mask.any() and flat_colors.size:
                c = flat_colors[mask].mean(axis=0)
            elif flat_colors.size:
                # faixa vazia → usa média global para não “quebrar” a sequência
                c = flat_colors.mean(axis=0)
            else:
                c = (0.85, 0.85, 0.85, 1.0)

            # garante tupla de floats (RGBA) no range [0,1]
            c = tuple(float(x) for x in np.asarray(c).tolist())
            if max(c) > 1.0:
                c = tuple(x / 255.0 for x in c)
            if len(c) == 3:
                c = (*c, 1.0)

            band_colors.append(c)

        cb.update_discrete(labels, band_colors)

    def load_and_draw_buffers(self, vertices_np, faces_np):
        """
        Desenha a malha diretamente de arrays (vértices/faces), sem ler DAE.
        Robustez: não depende da existência de controles específicos da UI; usa defaults.
        """

        # helpers para ler UI com segurança
        def _ui_bool(names, default=False):
            for n in names:
                w = getattr(self, n, None)
                if hasattr(w, "isChecked"):
                    try:
                        return bool(w.isChecked())
                    except Exception:
                        pass
            return default

        def _ui_int(names, default):
            for n in names:
                w = getattr(self, n, None)
                if hasattr(w, "value"):
                    try:
                        return int(w.value())
                    except Exception:
                        pass
            return default

        # Remove item anterior, se houver
        try:
            if getattr(self, "mesh_item", None) is not None:
                self.view.removeItem(self.mesh_item)
                self.mesh_item = None
        except Exception:
            pass

        # Guarda Z ORIGINAL (para colorbar/legenda)
        self._z_raw = vertices_np[:, 2].copy()
        self._zmin_raw = float(self._z_raw.min()) if self._z_raw.size else 0.0
        self._zmax_raw = float(self._z_raw.max()) if self._z_raw.size else 0.0

        # Centraliza apenas para desenhar (mantém z_raw intacto para simbologia)
        if vertices_np.size:
            cx = (vertices_np[:, 0].min() + vertices_np[:, 0].max()) / 2.0
            cy = (vertices_np[:, 1].min() + vertices_np[:, 1].max()) / 2.0
            cz = (vertices_np[:, 2].min() + vertices_np[:, 2].max()) / 2.0
            vertices_np = vertices_np.copy()
            vertices_np[:, 0] -= cx
            vertices_np[:, 1] -= cy
            vertices_np[:, 2] -= cz

        # Lê configurações da UI com defaults seguros
        draw_edges = _ui_bool(["checkbox_linhas", "checkBoxLinhas", "cbLinhas", "chkLinhas"], True)
        bg_white   = _ui_bool(["radio_white", "rbWhite", "radioButtonWhite", "rbBranco"], True)
        num_faixas = _ui_int (["slider_faixas", "sldFaixas", "horizontalSliderFaixas", "sliderFaixas"], 7)
        inverter   = _ui_bool(["checkbox_inverter_cores", "checkBoxInverterCores", "cbInvert", "chkInvert"], False)

        # Cores por faixas (usa método da classe se existir; senão cai no fallback)
        z_values = self._z_raw
        try:
            if hasattr(self, "criar_simbologia_gradiente_discreta"):
                colors = self.criar_simbologia_gradiente_discreta(z_values, num_faixas, inverter)
                colors = np.asarray(colors, dtype=float)
                if colors.size and colors.max() > 1.0:
                    colors = colors / 255.0
            else:
                # Fallback: escala de cinza simples
                if z_values.size:
                    zmin, zmax = float(z_values.min()), float(z_values.max())
                    den = max(zmax - zmin, 1e-9)
                    t = (z_values - zmin) / den
                    colors = np.c_[t, t, t, np.ones_like(t)]
                else:
                    colors = np.zeros((0, 4), dtype=float)
        except Exception:
            # Fallback de segurança, mesmo se algo der errado na geração de cores
            if z_values.size:
                zmin, zmax = float(z_values.min()), float(z_values.max())
                den = max(zmax - zmin, 1e-9)
                t = (z_values - zmin) / den
                colors = np.c_[t, t, t, np.ones_like(t)]
            else:
                colors = np.zeros((0, 4), dtype=float)

        # Corrige orientação (compatível com o caminho DAE)
        if faces_np.size:
            faces_np = faces_np[:, ::-1]

        # MeshData + normais
        self.mesh_data = gl.MeshData(vertexes=vertices_np, faces=faces_np, vertexColors=colors)
        try:
            _ = self.mesh_data.vertexNormals()
        except Exception:
            pass

        # Shader (usa método se existir; senão 'shaded')
        shader_obj = "shaded"
        try:
            if hasattr(self, "_get_phong_rim_shader"):
                shader_obj = self._get_phong_rim_shader()
        except Exception:
            shader_obj = "shaded"

        edge = (0.12, 0.12, 0.12, 1.0) if bg_white else (0.9, 0.9, 0.9, 1.0)

        self.mesh_item = gl.GLMeshItem(
            meshdata=self.mesh_data,
            smooth=True,
            drawFaces=True,
            drawEdges=draw_edges,
            edgeColor=edge,
            shader=shader_obj,
            glOptions="opaque")
        self.mesh_item.scale(10, 10, 10)

        self.view.addItem(self.mesh_item)

        try:
            self.view.setBackgroundColor("w" if bg_white else "k")
        except Exception:
            pass

        # Atualiza colorbar/eixos se os métodos existirem
        try:
            if hasattr(self, "_update_colorbar"):
                self._update_colorbar()
        except Exception:
            pass

        # guarda para futuros refreshes a partir de sinais da UI (se existirem)
        self._vertex_colors = colors

        # atualiza a colorbar imediatamente com as mesmas faixas/cores usadas no mesh
        self._update_colorbar_from_arrays(self._z_raw, self._vertex_colors, num_faixas)

        # (se você ainda chama algo como _ensure_axes_after_mesh_change(), mantenha)
        try:
            if hasattr(self, "_ensure_axes_after_mesh_change"):
                self._ensure_axes_after_mesh_change()
        except Exception:
            pass

    def decrease_slider_value(self):
        v = self.slider_faixas.value()
        if v > self.slider_faixas.minimum():
            self.slider_faixas.setValue(v - 1)  # spinbox segue pela conexão

    def increase_slider_value(self):
        v = self.slider_faixas.value()
        if v < self.slider_faixas.maximum():
            self.slider_faixas.setValue(v + 1)

    def salvar_visualizacao_como_png(self):
        """
        Salva a visualização 3D atual como uma imagem PNG.

        Esta função usa o método escolher_local_para_salvar de UiManagerM para permitir que o
        usuário escolha o local e o nome do arquivo PNG. Depois de capturar a imagem da área 3D
        (GLViewWidget), ela é salva no local escolhido. Se o processo for concluído com sucesso, uma
        mensagem de sucesso é exibida; caso contrário, uma mensagem de erro é exibida.

        Parâmetros:
        - Nenhum parâmetro explícito. A função usa o estado interno do objeto para capturar e salvar a visualização.

        Retorno:
        - Nenhum retorno explícito. Salva um arquivo PNG no local escolhido ou exibe mensagens de erro em caso de falha.
        """
        # Usar a função escolher_local_para_salvar da classe UiManagerM para obter o caminho do arquivo
        file_path = self.ui_manager.escolher_local_para_salvar("visualizacao_malha", "PNG Files (*.png)")
        
        if not file_path:
            # Se o usuário cancelar a operação, mostrar uma mensagem de erro e sair
            self.ui_manager.mostrar_mensagem("Operação cancelada pelo usuário.", "Erro")
            return  # Se não houver caminho, interrompe a função

        # Capturar a visualização da área 3D e salvar como PNG
        try:
            # Capturar a imagem da visualização 3D (tamanho atual da janela)
            img = self.view.grabFramebuffer()  # Captura o conteúdo da área 3D como uma imagem
            
            # Salvar a imagem no caminho escolhido com formato PNG
            img.save(file_path, 'PNG')

            # Mostrar uma mensagem de sucesso usando a função mostrar_mensagem de UiManagerM
            self.ui_manager.mostrar_mensagem(
                f"Visualização salva com sucesso em {file_path}", "Sucesso", 
                caminho_pasta=os.path.dirname(file_path), caminho_arquivo=file_path
            )
        except Exception as e:
            # Se houver um erro, mostrar a mensagem de erro
            self.ui_manager.mostrar_mensagem(f"Erro ao salvar a visualização: {str(e)}", "Erro")

    def remover_eixos_xyz(self):
        """
        Remove os eixos XYZ da visualização 3D.

        Esta função verifica se os eixos XYZ foram previamente adicionados à visualização. 
        Se os eixos estiverem presentes, eles são removidos da cena 3D, e a variável que os armazena 
        (`self.eixos_xyz`) é redefinida para None.

        Parâmetros:
        - Nenhum parâmetro explícito. A função opera sobre o estado atual da visualização 3D.

        Retorno:
        - Nenhum retorno explícito. Remove os eixos da visualização ou não faz nada se os eixos não existirem.
        """
        # Verifica se os eixos XYZ foram previamente adicionados (self.eixos_xyz não é None)
        if self.eixos_xyz:
            # Itera sobre os eixos (X, Y e Z) armazenados e remove cada um da visualização
            for eixo in self.eixos_xyz:
                self.view.removeItem(eixo)  # Remove o item da visualização 3D
            
            # Após remover, redefine a variável eixos_xyz para None, indicando que não há mais eixos na visualização
            self.eixos_xyz = None

    def alternar_eixos_xyz(self, state):
        """
        Alterna a exibição dos eixos XYZ na visualização 3D com base no estado de um checkbox.

        Esta função é usada para:
        - Controlar a adição ou remoção dos eixos XYZ da cena 3D.
        - Se o checkbox estiver marcado (`Qt.Checked`):
          - Os eixos XYZ são adicionados à visualização.
        - Se o checkbox estiver desmarcado (qualquer outro estado):
          - Os eixos XYZ são removidos da visualização.

        Parâmetros:
        - state (int): O estado do checkbox, que pode ser `Qt.Checked` (marcado) ou `Qt.Unchecked` (desmarcado).

        Retorno:
        - Nenhum retorno explícito. Adiciona ou remove os eixos da visualização com base no estado do checkbox.
        """
        # Verifica o estado do checkbox para adicionar ou remover os eixos XYZ
        if state == Qt.CheckState.Checked:
            # Se o checkbox estiver marcado, adiciona os eixos XYZ à visualização
            self.adicionar_eixos_xyz()
        else:
            # Se o checkbox estiver desmarcado, remove os eixos XYZ da visualização
            self.remover_eixos_xyz()

    def adicionar_eixos_xyz(self):
        """
        Adiciona os eixos XYZ à visualização 3D.

        Esta função cria três linhas coloridas que representam os eixos X, Y e Z, e as adiciona
        à cena 3D para ajudar a orientar o usuário sobre a posição e a escala dos objetos renderizados.

        Ações realizadas:
        - Define as cores para cada eixo:
          - Vermelho para o eixo X.
          - Verde para o eixo Y.
          - Azul para o eixo Z.
        - Define o comprimento de cada eixo.
        - Cria as linhas que representam cada eixo com as cores e comprimentos definidos.
        - Adiciona os eixos à visualização 3D.
        - Armazena os objetos de linha (eixos) em uma lista para possível remoção futura.

        Parâmetros:
        - Nenhum parâmetro explícito. A função opera sobre o estado atual da visualização 3D.

        Retorno:
        - Nenhum retorno explícito. Os eixos são adicionados diretamente à cena 3D.
        """

        if getattr(self, "eixos_xyz", None):
            return  # já existem, não duplica

        # Definindo as cores para cada eixo
        cor_x = (1, 0, 0, 1)  # Vermelho para o eixo X
        cor_y = (0, 1, 0, 1)  # Verde para o eixo Y
        cor_z = (0, 0, 1, 1)  # Azul para o eixo Z

        # Comprimento dos eixos
        tamanho_eixo = 120  # Ajuste conforme necessário para definir o comprimento dos eixos

        # Coordenadas para o eixo X
        eixo_x = np.array([[0, 0, 0], [tamanho_eixo, 0, 0]])  # Linha do eixo X, do ponto (0, 0, 0) até (120, 0, 0)
        # Criando o item para o eixo X
        linha_x = GLLinePlotItem(pos=eixo_x, color=cor_x, width=2, antialias=True)  # Linha do eixo X com cor e suavização

        # Coordenadas para o eixo Y
        eixo_y = np.array([[0, 0, 0], [0, tamanho_eixo, 0]])  # Linha do eixo Y, do ponto (0, 0, 0) até (0, 120, 0)
        # Criando o item para o eixo Y
        linha_y = GLLinePlotItem(pos=eixo_y, color=cor_y, width=2, antialias=True)  # Linha do eixo Y com cor e suavização

        # Coordenadas para o eixo Z
        eixo_z = np.array([[0, 0, 0], [0, 0, tamanho_eixo]])  # Linha do eixo Z, do ponto (0, 0, 0) até (0, 0, 120)
        # Criando o item para o eixo Z
        linha_z = GLLinePlotItem(pos=eixo_z, color=cor_z, width=2, antialias=True)  # Linha do eixo Z com cor e suavização

        # Armazena os eixos em uma lista para controle posterior (remover ou alterar)
        self.eixos_xyz = [linha_x, linha_y, linha_z]

        # Mantém eixos sempre visíveis (sem depth test)
        try:
            linha_x.setGLOptions('translucent')
            linha_y.setGLOptions('translucent')
            linha_z.setGLOptions('translucent')
        except Exception:
            pass

        # Adicionando as linhas dos eixos à visualização 3D
        self.view.addItem(linha_x)  # Adiciona o eixo X à visualização
        self.view.addItem(linha_y)  # Adiciona o eixo Y à visualização
        self.view.addItem(linha_z)  # Adiciona o eixo Z à visualização

    def decrease_slider_value(self):
        """
        Diminui o valor atual do slider de faixas de cores em 1 unidade.

        Esta função verifica o valor atual do slider (`self.slider_faixas`). Se o valor atual for maior
        que o valor mínimo permitido pelo slider, ele é diminuído em 1 unidade.

        Ações realizadas:
        - Verifica o valor atual do slider.
        - Compara o valor atual com o valor mínimo permitido.
        - Se o valor atual for maior que o mínimo, diminui o valor do slider em 1 unidade.

        Parâmetros:
        - Nenhum parâmetro explícito. A função opera sobre o estado atual do slider de faixas de cores.

        Retorno:
        - Nenhum retorno explícito. Atualiza o valor do slider diretamente.
        """
        # Obtém o valor atual do slider de faixas de cores
        current_value = self.slider_faixas.value()
        
        # Verifica se o valor atual é maior que o valor mínimo permitido pelo slider
        if current_value > self.slider_faixas.minimum():
            # Se for maior, diminui o valor do slider em 1 unidade
            self.slider_faixas.setValue(current_value - 1)

    def increase_slider_value(self):
        """
        Aumenta o valor atual do slider de faixas de cores em 1 unidade.

        Esta função verifica o valor atual do slider (`self.slider_faixas`). Se o valor atual for menor
        que o valor máximo permitido pelo slider, ele é aumentado em 1 unidade.

        Ações realizadas:
        - Verifica o valor atual do slider.
        - Compara o valor atual com o valor máximo permitido.
        - Se o valor atual for menor que o máximo, aumenta o valor do slider em 1 unidade.

        Parâmetros:
        - Nenhum parâmetro explícito. A função opera sobre o estado atual do slider de faixas de cores.

        Retorno:
        - Nenhum retorno explícito. Atualiza o valor do slider diretamente.
        """
        # Obtém o valor atual do slider de faixas de cores
        current_value = self.slider_faixas.value()

        # Verifica se o valor atual é menor que o valor máximo permitido pelo slider
        if current_value < self.slider_faixas.maximum():
            # Se for menor, aumenta o valor do slider em 1 unidade
            self.slider_faixas.setValue(current_value + 1)

    def mudar_fundo(self):
        """
        Altera a cor de fundo da visualização 3D com base no RadioButton selecionado.

        Esta função verifica qual dos RadioButtons está selecionado: se o fundo branco ou o fundo preto.
        Dependendo da seleção, a função altera a cor de fundo da visualização 3D para branco ou preto.
        Além disso, para manter a legibilidade da malha, ela recria o objeto GLMeshItem ajustando
        automaticamente a cor das arestas (edges) de acordo com o fundo escolhido.

        Ações realizadas:
        - Verifica qual RadioButton está marcado:
          - Se "Fundo Branco": define fundo branco e usa arestas escuras.
          - Se "Fundo Preto": define fundo preto e usa arestas claras.
        - Remove o GLMeshItem atual (se existir) e recria-o com a cor de aresta adequada.
        - Mantém todas as demais características da malha (dados, escala, shader e checkbox de arestas).

        Parâmetros:
        - Nenhum. Opera sobre o estado dos RadioButtons e do GLMeshItem atual.

        Retorno:
        - Nenhum. Atualiza diretamente a visualização 3D.
        """
        # Parte 1: definir cor do fundo
        # Se o RadioButton de fundo branco estiver selecionado
        if self.radio_white.isChecked():
            # Define a cor de fundo como branca
            self.view.setBackgroundColor('w')
        # Se o RadioButton de fundo preto estiver selecionado
        elif self.radio_black.isChecked():
            # Define a cor de fundo como preta
            self.view.setBackgroundColor('k')

        # Parte 2: recriar o GLMeshItem para ajustar arestas
        # Só faz a operação se já houver malha carregada (mesh_data)
        if self.mesh_item is not None and self.mesh_data is not None:
            # Remove o item antigo da cena para evitar sobreposição
            self.view.removeItem(self.mesh_item)

            # Define cor das arestas de forma adaptativa:
            # - Fundo branco → arestas escuras (preto suave)
            # - Fundo preto  → arestas claras (branco suave)
            edge = (0.1, 0.1, 0.1, 1.0) if self.radio_white.isChecked() else (0.9, 0.9, 0.9, 1.0)

            # # Recria o objeto da malha 3D com as mesmas configurações
            self.mesh_item = gl.GLMeshItem(
                meshdata=self.mesh_data,
                smooth=False,
                drawEdges=self.checkbox_edges.isChecked(),
                edgeColor=edge,
                shader=self._get_phong_rim_shader(),   # <<< aqui (antes estava 'shaded')
                glOptions='opaque')

            # Escala da malha (mantém a mesma do carregamento inicial)
            self.mesh_item.scale(10, 10, 10)

            # Adiciona novamente a malha à visualização 3D
            self.view.addItem(self.mesh_item)

    def enquadrar_malha(self, vertices_np):
        """
        Ajusta a posição da câmera e centraliza a visualização da malha 3D com base nas coordenadas dos vértices.

        Esta função calcula os limites (bounding box) da malha 3D a partir das coordenadas dos vértices fornecidos.
        Com esses limites, a função ajusta a distância da câmera e centraliza a visualização da malha para garantir
        que toda a malha esteja visível no visualizador.

        Ações realizadas:
        - Calcula os limites mínimos e máximos da malha (bounding box).
        - Ajusta a distância da câmera para que toda a malha seja visível.
        - Centraliza a visualização da malha no centro dos limites calculados.

        Parâmetros:
        - vertices_np (numpy.ndarray): Um array NumPy contendo as coordenadas dos vértices da malha (Nx3),
          onde N é o número de vértices e 3 são as coordenadas X, Y e Z de cada vértice.

        Retorno:
        - Nenhum retorno explícito. A câmera é ajustada diretamente para enquadrar a malha.
        """
        # Calcular os limites mínimos e máximos da malha (bounding box)
        min_bounds = vertices_np.min(axis=0)  # Limites mínimos para X, Y, Z
        max_bounds = vertices_np.max(axis=0)  # Limites máximos para X, Y, Z
        size = max_bounds - min_bounds  # Calcula o tamanho da malha ao subtrair os limites mínimos dos máximos

        # Ajustar a distância da câmera para garantir que toda a malha esteja visível
        # A distância da câmera é ajustada com base na norma do tamanho da malha, multiplicada por um fator (3)
        self.view.opts['distance'] = np.linalg.norm(size) * 3

        # centro real da bbox:
        center = (min_bounds + max_bounds) / 2.0
        self.view.opts['center'] = pg.Vector(center[0], center[1], center[2])

    def alternar_linhas_vertices(self):
        """
        Atualiza a exibição das linhas dos vértices (arestas) da malha 3D.

        Esta função verifica se os dados da malha (mesh_data) estão disponíveis. Se estiverem, a função remove
        a malha atual da visualização e a recria com a opção de exibir ou ocultar as linhas das arestas com base no estado do checkbox.
        A exibição das arestas é controlada pelo checkbox `self.checkbox_edges`.

        Ações realizadas:
        - Verifica se a malha (mesh_data) está carregada.
        - Remove o item da malha atual da visualização.
        - Recria o item da malha com a opção de desenhar ou não as arestas, dependendo do estado do checkbox.
        - Adiciona novamente o item da malha à visualização.
        - Atualiza a visualização para refletir as mudanças.

        Parâmetros:
        - Nenhum parâmetro explícito. A função opera sobre o estado atual da visualização 3D e o checkbox.

        Retorno:
        - Nenhum retorno explícito. Atualiza a visualização 3D diretamente.
        """
        # Verifica se os dados da malha estão carregados
        if self.mesh_data is not None:
            # Remove o item da malha atual da visualização
            self.view.removeItem(self.mesh_item)  # Remove o item da malha atual

            # Recria o item da malha com a opção de desenhar as arestas (drawEdges) controlada pelo checkbox
            edge = (0.1, 0.1, 0.1, 0.9) if self.radio_white.isChecked() else (0.9, 0.9, 0.9, 1.0)

            self.mesh_item = gl.GLMeshItem(meshdata=self.mesh_data, smooth=False, drawEdges=self.checkbox_edges.isChecked(), edgeColor=edge, shader=self._get_phong_rim_shader(), glOptions='opaque')

            # Ajusta o tamanho da malha
            self.mesh_item.scale(10, 10, 10)

            # Adiciona o item da malha de volta à visualização
            self.view.addItem(self.mesh_item)

            # Atualiza a visualização para aplicar as mudanças
            self.view.update()

    def _get_phong_rim_shader(self):
        """
        Shader Phong + rim (silhueta) com iluminação two-sided.
        - Usa faceforward para orientar a normal para a câmera.
        - Mantém a cor por vértice (vColor) que você já aplica pelo gradiente de Z.
        - Se ShaderProgram não estiver disponível, faz fallback para 'shaded'.
        """
        if hasattr(self, "_phong_rim_cached"):
            return self._phong_rim_cached

        try:
            vshader = """
            varying vec3 vNormal;
            varying vec3 vPos;
            varying vec4 vColor;
            void main() {
                gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
                vPos    = (gl_ModelViewMatrix * gl_Vertex).xyz;
                vNormal = normalize(gl_NormalMatrix * gl_Normal);
                vColor  = gl_Color;
            }
            """;

            # // Iluminação two-sided: usa normal virada para o observador.
            fshader = """
            varying vec3 vNormal;
            varying vec3 vPos;
            varying vec4 vColor;
            uniform vec3  light_dir;   // em espaço de view
            uniform float ambient;
            uniform float diffuse;
            uniform float specular;
            uniform float shininess;
            uniform float rim;

            void main() {
                vec3 N = normalize(vNormal);
                vec3 V = normalize(-vPos);                 // direção p/ câmera
                vec3 Nf = faceforward(N, -V, N);           // vira normal p/ a câmera (two-sided)
                vec3 L = normalize(light_dir);             // luz desde a câmera
                vec3 H = normalize(L + V);

                float lambert = max(dot(Nf, L), 0.0);      // difuso em ambas as faces
                float specHL  = pow(max(dot(Nf, H), 0.0), shininess);

                // Rim realça silhueta dos dois lados (usa Nf)
                float rimTerm = pow(1.0 - max(dot(Nf, V), 0.0), 2.0) * rim;

                vec3 base = vColor.rgb;                    // cor já vinda do seu gradiente por Z
                vec3 col  = base * (ambient + diffuse * lambert)
                            + specular * specHL * vec3(1.0)
                            + rimTerm * vec3(0.25);

                gl_FragColor = vec4(col, vColor.a);
            }
            """;

            sp = ShaderProgram('phong_rim_twosided', [vshader, fshader])
            # parâmetros próximos do 3dviewer
            sp.setUniform('light_dir', (0.0, 0.0, 1.0))
            sp.setUniform('ambient',   0.35)
            sp.setUniform('diffuse',   0.85)
            sp.setUniform('specular',  0.20)
            sp.setUniform('shininess', 48.0)
            sp.setUniform('rim',       0.65)

            self._phong_rim_cached = sp
            return sp

        except Exception:
            self._phong_rim_cached = 'shaded'  # fallback (sem two-sided custom)
            return 'shaded'

    def criar_simbologia_gradiente_discreta(self, valores, num_faixas=5, inverter_gradiente=False):
        """
        Cria um gradiente discreto de cores baseado nos valores fornecidos para os vértices da malha.

        Esta função gera um gradiente de cores aplicável aos vértices da malha, com base nos valores Z fornecidos
        (ou outra propriedade dos vértices). As cores são distribuídas em faixas discretas e aplicadas aos vértices
        de acordo com o número de faixas selecionado. As cores podem ser invertidas se o parâmetro inverter_gradiente
        for verdadeiro.

        Ações realizadas:
        - Calcula os limites mínimos e máximos dos valores fornecidos.
        - Divide os valores em faixas discretas com base no número de faixas selecionado.
        - Aplica cores pré-definidas às faixas.
        - Opcionalmente, inverte o gradiente de cores se o checkbox de inversão estiver marcado.
        - Aplica a cor correspondente a cada vértice com base na faixa em que o valor se encontra.

        Parâmetros:
        - valores (numpy.ndarray): Um array contendo os valores (geralmente Z) dos vértices da malha.
        - num_faixas (int): O número de faixas discretas de cores a serem aplicadas (padrão: 5).
        - inverter_gradiente (bool): Se True, inverte o gradiente de cores (padrão: False).

        Retorno:
        - cores (numpy.ndarray): Um array contendo as cores RGBA para cada vértice, aplicadas com base no gradiente.
        """
        # Calcula os valores mínimos e máximos dos vértices (normalmente os valores Z)
        min_valor = min(valores)  # Valor mínimo no array de valores
        max_valor = max(valores)  # Valor máximo no array de valores

        # Dividindo os valores em 'num_faixas' faixas discretas
        # Gera um array com os limites das faixas, do valor mínimo ao máximo
        faixas = np.linspace(min_valor, max_valor, num_faixas + 1)

        # Cores pré-definidas para as faixas
        cores_faixa_predefinidas = [
            (1.0, 0.0, 0.0, 1.0),  # Vermelho
            (1.0, 0.3, 0.0, 1.0),  # Vermelho-laranja
            (1.0, 0.5, 0.0, 1.0),  # Laranja
            (1.0, 0.75, 0.0, 1.0), # Amarelo-alaranjado
            (1.0, 1.0, 0.0, 1.0),  # Amarelo
            (0.5, 1.0, 0.0, 1.0),  # Amarelo-esverdeado
            (0.0, 1.0, 0.0, 1.0),  # Verde
            (0.0, 1.0, 0.5, 1.0),  # Verde-azulado
            (0.0, 0.5, 1.0, 1.0),  # Azul claro
            (0.0, 0.0, 0.8, 1.0)]   # Azul
        

        total_cores = len(cores_faixa_predefinidas)  # Número total de cores predefinidas

        # Selecionar 'num_faixas' cores distribuídas uniformemente ao longo das cores predefinidas
        # Os índices das cores são escolhidos de forma a cobrir o total de cores disponível
        indices_cores = np.linspace(0, total_cores - 1, num_faixas).astype(int)
        cores_faixa = [cores_faixa_predefinidas[i] for i in indices_cores]  # Seleciona as cores para as faixas

        # Inverter as cores se o parâmetro inverter_gradiente for True
        if inverter_gradiente:
            cores_faixa = cores_faixa[::-1]  # Inverte a ordem das cores

        # Inicializa um array de zeros para armazenar as cores de cada vértice (RGBA)
        cores = np.zeros((len(valores), 4))  # Cria um array para armazenar as cores para cada vértice

        # Aplica a cor correspondente a cada vértice com base no valor
        for i, valor in enumerate(valores):
            # Percorre as faixas de valores para encontrar em qual faixa o valor se encaixa
            for j in range(num_faixas):
                if faixas[j] <= valor < faixas[j + 1]:
                    cores[i] = cores_faixa[j]  # Atribui a cor correspondente à faixa
                    break
            else:
                cores[i] = cores_faixa[-1]  # Atribui a última cor se o valor for o máximo ou além

        return cores  # Retorna o array de cores para cada vértice

    def ver_tudo(self):
        """
        Mostra toda a visualização:
          1) Calcula o bounding box de tudo na cena (malha escalada + eixos).
          2) Aplica margem.
          3) Centraliza e ajusta a distância da câmera.
          4) Restaura um ângulo padrão (opcional) e atualiza a view.
        """
        try:
            mins, maxs = self._scene_bounds()
            center = (mins + maxs) * 0.5
            size   = (maxs - mins)

            # margem (15%) para evitar recorte nas bordas
            margin = 1.15
            size *= margin

            # define distância com base no maior lado da bbox
            # (um pouco maior para garantir o enquadramento)
            diag = float(np.linalg.norm(size))
            dist = max(1.0, diag * 0.9)  # ajuste fino aqui se quiser

            # aplica center/distance direto nos opts da view
            self.view.opts['center']   = pg.Vector(center[0], center[1], center[2])
            self.view.opts['distance'] = dist

            # (opcional) Ângulo padrão suave de visualização
            # Ex.: azim=45°, elev=30° — ajuste ao seu gosto
            self.view.setCameraPosition(azimuth=45, elevation=30)

            # garante um FOV “saudável” (padrão 60)
            self.view.opts['fov'] = 60.0

            self.view.update()
        except Exception as e:
            if self.ui_manager:
                self.ui_manager.mostrar_mensagem(f"Não foi possível mostrar toda a visualização: {e}", "Erro")

    def _scene_bounds(self):
        """
        Retorna (min_xyz, max_xyz) do que existe na cena:
        - Malha (mesh_data) já considerando a escala aplicada ao GLMeshItem (10x).
        - Eixos XYZ (se estiverem presentes).
        """
        have_any = False
        mins = np.array([ np.inf,  np.inf,  np.inf], dtype=float)
        maxs = np.array([-np.inf, -np.inf, -np.inf], dtype=float)

        # Malha (considera escala do GLMeshItem)
        if self.mesh_data is not None and self.mesh_item is not None:
            try:
                V = self.mesh_data.vertexes()  # (N,3) centrados no load
                # sua malha é escalada em 10x após adicionar:
                V = V * 10.0
                vmin = V.min(axis=0)
                vmax = V.max(axis=0)
                mins = np.minimum(mins, vmin); maxs = np.maximum(maxs, vmax)
                have_any = True
            except Exception:
                pass

        # Eixos (se existirem)
        if getattr(self, "eixos_xyz", None):
            try:
                # eixos foram criados como linhas entre [0,0,0] e [tamanho,0,0] etc.
                # então basta considerar o tamanho que você usou:
                tamanho_eixo = 120.0
                axis_pts = np.array([
                    [0, 0, 0], [tamanho_eixo, 0, 0],
                    [0, 0, 0], [0, tamanho_eixo, 0],
                    [0, 0, 0], [0, 0, tamanho_eixo]
                ], dtype=float)
                vmin = axis_pts.min(axis=0)
                vmax = axis_pts.max(axis=0)
                mins = np.minimum(mins, vmin); maxs = np.maximum(maxs, vmax)
                have_any = True
            except Exception:
                pass

        if not have_any:
            # fallback: volume pequeno ao redor da origem
            return np.array([-1.0, -1.0, -1.0]), np.array([1.0, 1.0, 1.0])
        return mins, maxs

    def _ensure_axes_after_mesh_change(self):
        """
        Garante eixos visíveis após recriar a malha.
        Remove e re-adiciona eixos se o checkbox_xyz estiver marcado.
        """
        if self.checkbox_xyz.isChecked():
            # remove para evitar duplicidades e re-adiciona “do zero”
            self.remover_eixos_xyz()
            self.adicionar_eixos_xyz()

    def load_and_draw_dae(self):
        """
        Carrega um DAE, aplica cores por Z (faixas), e desenha a malha com
        sombreamento semelhante ao 3dviewer: Phong + rim (quando disponível),
        mantendo o controle de arestas pelo checkbox.
        """
        # Remover item anterior (se houver) para evitar sobreposição/leak
        try:
            if getattr(self, "mesh_item", None) is not None:
                self.view.removeItem(self.mesh_item)
                self.mesh_item = None
        except Exception:
            pass

        vertices = []
        faces = []

        try:
            # 1) Ler DAE
            tree = ET.parse(self.dae_file_path)
            root = tree.getroot()
            ns = "{http://www.collada.org/2005/11/COLLADASchema}"

            # 2) Vértices
            for source in root.findall(f".//{ns}float_array"):
                if "positions" in source.attrib.get("id", ""):
                    raw = list(map(float, source.text.split()))
                    for i in range(0, len(raw), 3):
                        vertices.append([raw[i], raw[i+1], raw[i+2]])
                    break

            # 3) Faces (assumindo triângulos indexados simples)
            for p in root.findall(f".//{ns}p"):
                raw = list(map(int, p.text.split()))
                for i in range(0, len(raw), 3):
                    faces.append([raw[i], raw[i+1], raw[i+2]])

            if not vertices or not faces:
                self.ui_manager.mostrar_mensagem("Nenhuma geometria encontrada no DAE.", "Erro")
                return

            # 4) NumPy + centralizar
            vertices_np = np.array(vertices, dtype=float)

            faces_np    = np.array(faces, dtype=int)

            # Guarde Z ORIGINAL (pré-centralização) p/ cores/legenda ---
            self._z_raw = vertices_np[:, 2].copy()
            self._zmin_raw = float(self._z_raw.min())
            self._zmax_raw = float(self._z_raw.max())

            # centraliza apenas para desenhar
            center = vertices_np.mean(axis=0)
            vertices_np = vertices_np - center

            # 5) Cores por Z (faixas)  -> usa Z ORIGINAL
            z_values = self._z_raw
            num_faixas = self.slider_faixas.value()
            inverter = self.checkbox_inverter_cores.isChecked()
            colors = self.criar_simbologia_gradiente_discreta(z_values, num_faixas, inverter)

            # Garantir formato/intervalo das cores: (N,4) float [0..1]
            colors = np.asarray(colors, dtype=float)
            if colors.max() > 1.0:
                colors = colors / 255.0

            # 6) MeshData + normais (necessárias pro Phong/shaded)
            # Corrige orientação das faces (inverte índice)
            faces_np = faces_np[:, ::-1]
            self.mesh_data = gl.MeshData(vertexes=vertices_np, faces=faces_np, vertexColors=colors)
            try: # força o cálculo (ignora se já existir)
                _ = self.mesh_data.vertexNormals()
            except Exception:
                pass

            # 7) GLMeshItem com shader (Phong+rim se disponível, senão 'shaded')
            shader_obj = self._get_phong_rim_shader()  # pode devolver ShaderProgram ou 'shaded'
            edge = (0.12, 0.12, 0.12, 1.0) if self.radio_white.isChecked() else (0.9, 0.9, 0.9, 1.0)
            self.mesh_item = gl.GLMeshItem(meshdata=self.mesh_data, smooth=False, drawEdges=self.checkbox_edges.isChecked(), edgeColor=edge, shader=self._get_phong_rim_shader(), glOptions='opaque')

            # 8) Escala, adicionar e fundo
            self.mesh_item.scale(10, 10, 10)
            self.view.addItem(self.mesh_item)
            self.view.setBackgroundColor('w')

            # 9) Enquadrar
            self.enquadrar_malha(vertices_np)

            # Atualize a legenda discreta
            self._update_colorbar_discrete()

            self.ui_manager.mostrar_mensagem("Renderização da malha DAE concluída com sucesso.", "Sucesso")

        except Exception as e:
            self.ui_manager.mostrar_mensagem(f"Erro ao carregar/processar o DAE: {e}", "Erro")

    def atualizar_malha_com_faixas(self):
        """
        Atualiza a malha 3D com novas cores baseadas no número de faixas (slider) e
        na opção de inverter gradiente (checkbox).

        Fluxo principal:
        1. Verifica se existe `mesh_data` carregada.
        2. Lê valores de Z dos vértices da malha.
        3. Gera um conjunto de cores discretas (gradiente com faixas).
        4. Aplica essas cores diretamente na `mesh_data` (ou recria um `MeshData`).
        5. Remove o item antigo da cena e adiciona um novo com as cores atualizadas.
        6. Recoloca eixos XYZ, se estiverem habilitados.
        """
        if self.mesh_data is None:
            return  # não há malha carregada

        # 1) Captura parâmetros da interface
        inverter_cores = self.checkbox_inverter_cores.isChecked()
        num_faixas     = self.slider_faixas.value()

        try:
            # 2) Lê Z para colorir -> Z ORIGINAL, se disponível
            if hasattr(self, "_z_raw") and self._z_raw is not None:
                z_values = self._z_raw
            else:
                z_values = self.mesh_data.vertexes()[:, 2]  # fallback

            # 3) Gera as cores
            new_colors = self.criar_simbologia_gradiente_discreta(z_values, num_faixas, inverter_cores).astype(float)

            # Normaliza se vier em escala 0–255
            if new_colors.max() > 1.0:
                new_colors = new_colors / 255.0
        except Exception as e:
            self.ui_manager.mostrar_mensagem(f"Erro ao recalcular cores: {e}", "Erro")
            return

        # 4) Aplica cores na malha (se não suportar, recria MeshData)
        try:
            self.mesh_data.setVertexColors(new_colors)
            recreate_meshdata = False
        except Exception:
            recreate_meshdata = True

        if recreate_meshdata:
            try:
                vertices_np = self.mesh_data.vertexes().copy()  # <<< pegue os vértices atuais
                faces_np    = self.mesh_data.faces().copy()
                new_md = gl.MeshData(vertexes=vertices_np, faces=faces_np, vertexColors=new_colors)
                try:
                    _ = new_md.vertexNormals()
                except Exception:
                    pass
                self.mesh_data = new_md
            except Exception as e:
                self.ui_manager.mostrar_mensagem(f"Erro ao recriar MeshData: {e}", "Erro")
                return

        # 5) Recria o GLMeshItem com as novas cores
        try:
            if self.mesh_item is not None:
                self.view.removeItem(self.mesh_item)

            edge = (0.1, 0.1, 0.1, 1.0) if self.radio_white.isChecked() else (0.9, 0.9, 0.9, 1.0)
            self.mesh_item = gl.GLMeshItem(
                meshdata=self.mesh_data,
                smooth=False,
                drawEdges=self.checkbox_edges.isChecked(),
                edgeColor=edge,
                shader=self._get_phong_rim_shader(),
                glOptions='opaque')
            self.mesh_item.scale(10, 10, 10)
            self.view.addItem(self.mesh_item)

            # depois de redesenhar:
            self.view.update()
            self._update_colorbar_discrete()

            # 6) Recoloca eixos XYZ, se habilitados
            if self.checkbox_xyz.isChecked() and not getattr(self, "eixos_xyz", None):
                self.adicionar_eixos_xyz()
        except Exception as e:
            self.ui_manager.mostrar_mensagem(f"Erro ao atualizar a malha: {e}", "Erro")

    def _update_colorbar(self):
        """Sincroniza a barra de cores com Zmin/Zmax atuais e as faixas/ordem."""
        if self.mesh_data is None:
            return
        V = self.mesh_data.vertexes()
        zmin = float(np.min(V[:,2]))
        zmax = float(np.max(V[:,2]))
        num = self.slider_faixas.value()
        inv = self.checkbox_inverter_cores.isChecked()
        bands = self._band_colors_from_settings(num, inv)
        self.colorbar.update_scale(zmin, zmax, bands)

    def _fmt_br(self, v, dec=2):
        # formata 12.3456 -> "12,35"
        return f"{v:.{dec}f}".replace('.', ',')

    def _legend_edges_labels(self, zmin, zmax, n, inverter=False, dec=2):
        """
        Gera N rótulos SEM inverter a ordem:
          n=1   →  ["zmin - zmax"]
          n=2   →  ["<= e1", "> e1"]
          n>=3  →  ["<= e1", "e1 - e2-ε", ..., "> e(N-1)-ε"]
        """
        if n < 1:
            return [], []

        edges = np.linspace(zmin, zmax, n + 1)   # e0..eN
        eps   = 10 ** (-dec)

        if n == 1:
            labels = [f"{self._fmt_br(zmin, dec)} - {self._fmt_br(zmax, dec)}"]
            return edges, labels

        labels = []
        labels.append(f"<= {self._fmt_br(edges[1], dec)}")  # primeira faixa (baixa)

        if n == 2:
            labels.append(f"> {self._fmt_br(edges[1], dec)}")  # segunda (alta)
        else:
            for k in range(1, n - 1):
                lo = self._fmt_br(edges[k], dec)
                hi = self._fmt_br(edges[k + 1] - eps, dec)
                labels.append(f"{lo} - {hi}")
            labels.append(f"> {self._fmt_br(edges[-2] - eps, dec)}")  # última (alta)

        return edges, labels

    def _update_colorbar_discrete(self):
        """Atualiza a barra com rótulos por faixa usando Z ORIGINAL."""
        if not hasattr(self, "_z_raw") or self._z_raw is None:
            return

        zmin = float(self._z_raw.min())
        zmax = float(self._z_raw.max())
        inv  = self.checkbox_inverter_cores.isChecked()

        bands = self._band_colors_from_settings(self.slider_faixas.value(), inv)  # cores já invertidas quando preciso
        n     = len(bands)

        # rótulos SEM inversão (ordem baixa → alta)
        _, labels = self._legend_edges_labels(zmin, zmax, n, inverter=False, dec=2)

        if hasattr(self, "colorbar") and self.colorbar is not None:
            self.colorbar.update_discrete(labels, bands)

    def _band_colors_from_settings(self, num_faixas: int, inverter: bool):
        cores_predef = [
            (1.0, 0.0, 0.0, 1.0),
            (1.0, 0.3, 0.0, 1.0),
            (1.0, 0.5, 0.0, 1.0),
            (1.0, 0.75, 0.0, 1.0),
            (1.0, 1.0, 0.0, 1.0),
            (0.5, 1.0, 0.0, 1.0),
            (0.0, 1.0, 0.0, 1.0),
            (0.0, 1.0, 0.5, 1.0),
            (0.0, 0.5, 1.0, 1.0),
            (0.0, 0.0, 0.8, 1.0),
        ]
        total = len(cores_predef)
        idx = np.linspace(0, total - 1, num_faixas).astype(int)
        bands = [cores_predef[i] for i in idx]
        if inverter:
            bands = bands[::-1]
        return bands

    def closeEvent(self, e):
        try:
            if getattr(self, "mesh_item", None) is not None:
                self.view.removeItem(self.mesh_item)
                self.mesh_item = None
            if getattr(self, "_axes", None) is not None:
                self.view.removeItem(self._axes)
                self._axes = None
        except Exception:
            pass
        super().closeEvent(e)

class PannableGLViewWidget(gl.GLViewWidget):
    """
    Viewer 3D com:
      - Pan no botão direito (RMB), colado ao cursor, mesmo sentido do arrasto.
      - Rotação no botão esquerdo (LMB) mais SUAVE (sensibilidade menor + leve amortecimento).
    Ajustes finos:
      - self._sens       → sensibilidade do PAN (menor = mais lento)
      - self._rot_sens   → sensibilidade da ROTAÇÃO (deg por device-pixel; menor = mais lento)
      - self._rot_alpha  → suavização da rotação (0.1–0.3 = suave; 1.0 = sem suavização)
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
        self.setMouseTracking(True)
        self.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)

        # PAN (RMB)
        self._panning     = False
        self._lastPosPan  = None
        self._sens        = 0.15   # sensibilidade do pan (↓ p/ mais lento)

        # ROT (LMB)
        self._rotating    = False
        self._lastPosRot  = None
        self._rot_sx      = 0.0    # acumuladores p/ filtro (rotação)
        self._rot_sy      = 0.0
        self._rot_alpha   = 0.25   # suavização da rotação (0.1–0.3 deixa "mais suave")
        self._rot_sens    = 0.35   # deg por device-pixel (padrão do pyqtgraph ~0.5)

        try:
            self.setCursor(Qt.CursorShape.OpenHandCursor)
        except Exception:
            pass

    def _effective_dpr(self) -> float:
        try:
            return float(self.devicePixelRatioF())
        except Exception:
            return 1.0

    def _px_to_world_scale(self) -> float:
        """
        Converte 1 device-pixel em unidades do mundo (para PAN) usando distance/fov/viewport.
        """
        dist    = float(self.opts.get('distance', 200.0))
        fov_deg = float(self.opts.get('fov', 60.0))
        fov_rad = np.deg2rad(fov_deg)

        h = max(1, self.height())
        dpr = self._effective_dpr()
        h_eff = h * dpr

        win_h_world = 2.0 * dist * np.tan(fov_rad * 0.5)
        return (win_h_world / h_eff) * self._sens

    def mousePressEvent(self, ev):
        if ev.button() == Qt.MouseButton.RightButton:
            # Inicia PAN
            self._panning = True
            self._lastPosPan = ev.pos()
            try: self.setCursor(Qt.CursorShape.ClosedHandCursor)
            except Exception: pass
            ev.accept()
            return

        if ev.button() == Qt.MouseButton.LeftButton:
            # Inicia ROTAÇÃO (suave)
            self._rotating = True
            self._lastPosRot = ev.pos()
            self._rot_sx = 0.0
            self._rot_sy = 0.0
            ev.accept()
            return

        # Outros botões → comportamento padrão
        super().mousePressEvent(ev)

    def mouseMoveEvent(self, ev):
        # ROTAÇÃO (LMB)
        if self._rotating and self._lastPosRot is not None:
            raw = ev.pos() - self._lastPosRot
            self._lastPosRot = ev.pos()

            # converte para device px (corrige HiDPI)
            dpr = self._effective_dpr()
            dx = float(raw.x()) * dpr
            dy = float(raw.y()) * dpr

            # suavização leve (deixa "apenas mais suave")
            a = self._rot_alpha
            self._rot_sx = (1.0 - a) * self._rot_sx + a * dx
            self._rot_sy = (1.0 - a) * self._rot_sy + a * dy

            # limita picos de delta para evitar trancos
            max_px_step = 60.0
            sx = np.clip(self._rot_sx, -max_px_step, max_px_step)
            sy = np.clip(self._rot_sy, -max_px_step, max_px_step)

            # aplica rotação em GRAUS por device-pixel (mesma convenção do GLViewWidget):
            # → azim: -dx; elev: +dy
            self.orbit(-sx * self._rot_sens, sy * self._rot_sens)
            ev.accept()
            return

        # PAN (RMB)
        if self._panning and self._lastPosPan is not None:
            raw = ev.pos() - self._lastPosPan
            self._lastPosPan = ev.pos()

            # logical → device px
            dpr = self._effective_dpr()
            dx_px = float(raw.x()) * dpr
            dy_px = float(raw.y()) * dpr

            # px → mundo; mesmo sentido do mouse
            k = self._px_to_world_scale()
            dx_world =  dx_px * k      # direita → direita
            dy_world =  dy_px * k      # cima    → cima  (ajuste que você pediu)

            try:
                self.pan(dx_world, dy_world, 0.0, relative='view')
            except TypeError:
                self.pan(dx_world, dy_world, 0.0)

            ev.accept()
            return

        # sem pan/rot → padrão
        super().mouseMoveEvent(ev)

    def mouseReleaseEvent(self, ev):
        if ev.button() == Qt.MouseButton.RightButton and self._panning:
            self._panning = False
            self._lastPosPan = None
            try: self.setCursor(Qt.CursorShape.OpenHandCursor)
            except Exception: pass
            ev.accept()
            return

        if ev.button() == Qt.MouseButton.LeftButton and self._rotating:
            self._rotating = False
            self._lastPosRot = None
            ev.accept()
            return

        super().mouseReleaseEvent(ev)

class ColorBarWidget(QWidget):
    """
    Barra discreta com rótulos por faixa (estilo QGIS).
    Desenha todos os blocos, encolhendo-os se faltar altura.
    """
    def __init__(self, parent=None):
        super().__init__(parent)
        # sem altura fixa!
        self.setMinimumWidth(160)
        self._labels = []
        self._bands  = []
        # parâmetros base (podem ser ajustados)
        self._margin = 8
        self._gap    = 8
        self._box    = 22   # altura base de cada quadrado

    def update_discrete(self, labels, band_colors):
        self._labels = list(labels)
        norm = []
        for c in band_colors:
            if len(c) >= 4: r,g,b,a = c[:4]
            else:           r,g,b,a = (*c[:3], 1.0)
            norm.append((float(r), float(g), float(b), float(a)))
        # Robustez: garante pelo menos uma faixa placeholder
        if not norm:
            norm = [(0.85, 0.85, 0.85, 1.0)]
            if not self._labels:
                self._labels = [""]  # opcional: "Sem dados"

        self._bands = norm

        # define uma altura mínima suficiente para N faixas (sem compressão)
        n = max(1, len(self._bands))
        h_min = self._margin*2 + n*self._box + (n-1)*self._gap
        self.setMinimumHeight(int(h_min))
        self.updateGeometry()
        self.update()

    def sizeHint(self):
        # sugere altura baseada no nº de faixas
        n = max(1, len(self._bands))
        h = self._margin*2 + n*self._box + (n-1)*self._gap
        return QSize(180, int(h))

    def paintEvent(self, ev):
        """
        Desenha a barra de cores em faixas discretas.
        Robusto contra listas vazias ou desbalanceadas (_bands/_labels).
        """
        p = QPainter(self)
        p.setRenderHint(QPainter.RenderHint.Antialiasing, True)
        try:
            p.setRenderHint(QPainter.TextAntialiasing, True)
        except Exception:
            pass

        # número de faixas a desenhar (pelo menos 1 para manter layout estável)
        bands_len = len(self._bands) if getattr(self, "_bands", None) is not None else 0
        labels_len = len(self._labels) if getattr(self, "_labels", None) is not None else 0
        n = max(1, bands_len)

        margin = self._margin
        gap_y  = self._gap

        w = self.width()
        h = self.height()

        # Altura disponível; encolhe o bloco se precisar, mas nunca deixa minúsculo
        avail = max(1, h - 2*margin - (n - 1)*gap_y)
        box = min(self._box, avail / n)
        box = max(12.0, box)

        x_color = margin
        x_text  = int(x_color + box + 10)
        y = margin

        p.setFont(p.font())
        for i in range(n):
            # COR: segura mesmo se _bands for menor que n
            if i < bands_len:
                r, g, b, a = self._bands[i]
            else:
                r, g, b, a = (0.85, 0.85, 0.85, 1.0)  # placeholder

            p.setPen(Qt.PenStyle.NoPen)
            p.setBrush(QColor.fromRgbF(float(r), float(g), float(b), float(a)))
            p.drawRoundedRect(QRect(int(x_color), int(y), int(box), int(box)), 4, 4)

            # --- RÓTULO: já protegido pelo len ---
            label = str(self._labels[i]) if i < labels_len else ""
            p.setPen(QColor(40, 40, 40))
            p.drawText(QRect(x_text, int(y), int(w - x_text - margin), int(box)),
                       Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft, label)

            y += box + gap_y

        p.end()

class KMLStyleDialog(QDialog):
    """
    Esta classe cria um diálogo para a personalização dos estilos de exportação KML. O usuário pode definir
    a largura da linha, a transparência da linha, a cor da linha, a transparência das faces e a cor das faces.
    
    Detalhamento:
    1. Inicialização dos valores padrão para a cor da linha e a cor das faces.
    2. Configuração do layout principal do diálogo.
    3. Criação de um QFrame para manter os widgets de configuração.
    4. Adição de widgets para configurar a largura da linha.
    5. Adição de widgets para configurar a transparência da linha e escolher a cor da linha.
    6. Adição de widgets para configurar a transparência das faces e escolher a cor das faces.
    7. Adição de botões para confirmar ou cancelar a exportação.
    8. Configuração das interações, como a alteração da transparência da linha que desativa a largura da linha.

    Parâmetros:
    - parent: Widget pai do diálogo (padrão: None).

    Retorno:
    - Nenhum retorno direto. O diálogo coleta as configurações de estilo do usuário.

    Exceções tratadas:
    - Nenhuma exceção específica tratada nesta função.
    """
    
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Personalização de Estilos KML")
        
        self.line_color = QColor(0, 0, 255)  # Cor padrão azul
        self.face_color = QColor(0, 255, 0)  # Cor padrão verde

        # Layout principal
        main_layout = QVBoxLayout()  # Cria o layout vertical principal

        # QFrame
        frame = QFrame()  # Cria um frame
        frame.setFrameShape(QFrame.Shape.StyledPanel)  # Define o estilo do frame
        frame.setFrameShadow(QFrame.Shadow.Raised)  # Define a sombra do frame
        frame_layout = QVBoxLayout()  # Cria um layout vertical para o frame

        # Largura da Linha
        line_width_layout = QHBoxLayout()  # Cria um layout horizontal para a largura da linha
        line_width_label = QLabel("Largura da Linha:")  # Cria um rótulo
        self.line_width_spinbox = QDoubleSpinBox()  # Cria um spinbox para a largura da linha
        self.line_width_spinbox.setRange(0.5, 10.0)  # Define o intervalo de valores
        self.line_width_spinbox.setSingleStep(0.5)  # Define o passo dos valores
        self.line_width_spinbox.setValue(1.0)  # Define o valor padrão
        line_width_layout.addWidget(line_width_label)  # Adiciona o rótulo ao layout
        line_width_layout.addWidget(self.line_width_spinbox)  # Adiciona o spinbox ao layout
        
        # Opacidade da Linha
        line_opacity_layout = QHBoxLayout()  # Cria um layout horizontal para a opacidade da linha
        line_opacity_label = QLabel("Transparência das Linhas:")  # Cria um rótulo
        self.line_opacity_spinbox = QSpinBox()  # Cria um spinbox para a opacidade da linha
        self.line_opacity_spinbox.setRange(0, 100)  # Define o intervalo de valores
        self.line_opacity_spinbox.setSingleStep(5)  # Define o passo dos valores
        self.line_opacity_spinbox.setSuffix("%")  # Define o sufixo "%" para o spinbox
        self.line_opacity_spinbox.setValue(100)  # Define o valor padrão
        self.line_opacity_spinbox.valueChanged.connect(self.update_line_width_state)  # Conecta o sinal de alteração de valor ao método update_line_width_state
        self.line_color_button = QPushButton("Cor")  # Cria um botão para escolher a cor da linha
        self.line_color_button.setFixedSize(30, 20)  # Define o tamanho fixo do botão
        self.line_color_button.clicked.connect(self.choose_line_color)  # Conecta o clique do botão ao método choose_line_color
        self.update_button_color(self.line_color_button, self.line_color)  # Atualiza a cor do botão
        line_opacity_layout.addWidget(line_opacity_label)  # Adiciona o rótulo ao layout
        line_opacity_layout.addWidget(self.line_opacity_spinbox)  # Adiciona o spinbox ao layout
        line_opacity_layout.addWidget(self.line_color_button)  # Adiciona o botão ao layout
        
        # Transparência das Faces
        face_opacity_layout = QHBoxLayout()  # Cria um layout horizontal para a opacidade das faces
        face_opacity_label = QLabel("Transparência das Faces:")  # Cria um rótulo
        self.face_opacity_spinbox = QSpinBox()  # Cria um spinbox para a opacidade das faces
        self.face_opacity_spinbox.setRange(0, 100)  # Define o intervalo de valores
        self.face_opacity_spinbox.setSingleStep(5)  # Define o passo dos valores
        self.face_opacity_spinbox.setSuffix("%")  # Define o sufixo "%" para o spinbox
        self.face_opacity_spinbox.setValue(50)  # Define o valor padrão
        self.face_color_button = QPushButton("Cor")  # Cria um botão para escolher a cor das faces
        self.face_color_button.setFixedSize(30, 20)  # Define o tamanho fixo do botão
        self.face_color_button.clicked.connect(self.choose_face_color)  # Conecta o clique do botão ao método choose_face_color
        self.update_button_color(self.face_color_button, self.face_color)  # Atualiza a cor do botão
        face_opacity_layout.addWidget(face_opacity_label)  # Adiciona o rótulo ao layout
        face_opacity_layout.addWidget(self.face_opacity_spinbox)  # Adiciona o spinbox ao layout
        face_opacity_layout.addWidget(self.face_color_button)  # Adiciona o botão ao layout
        
        # Botões
        self.ok_button = QPushButton("Exportar")  # Cria o botão de confirmação
        self.ok_button.clicked.connect(self.accept)  # Conecta o clique do botão ao método accept
        self.cancel_button = QPushButton("Cancelar")  # Cria o botão de cancelamento
        self.cancel_button.clicked.connect(self.reject)  # Conecta o clique do botão ao método reject
        buttons_layout = QHBoxLayout()  # Cria um layout horizontal para os botões
        buttons_layout.addWidget(self.ok_button)  # Adiciona o botão de confirmação ao layout
        buttons_layout.addWidget(self.cancel_button)  # Adiciona o botão de cancelamento ao layout
        
        # Adicionando ao frame layout
        frame_layout.addLayout(line_width_layout)  # Adiciona o layout da largura da linha ao layout do frame
        frame_layout.addLayout(line_opacity_layout)  # Adiciona o layout da opacidade da linha ao layout do frame
        frame_layout.addLayout(face_opacity_layout)  # Adiciona o layout da opacidade das faces ao layout do frame
        frame_layout.addLayout(buttons_layout)  # Adiciona o layout dos botões ao layout do frame
        frame.setLayout(frame_layout)  # Define o layout do frame
        
        # Adicionando ao layout principal
        main_layout.addWidget(frame)  # Adiciona o frame ao layout principal
        self.setLayout(main_layout)  # Define o layout principal do diálogo

    def choose_line_color(self):
        """
        Abre um diálogo de seleção de cores para escolher a cor da linha.
        Atualiza a cor do botão de seleção de cor da linha se uma cor válida for escolhida.

        Detalhamento:
        1. Abre o diálogo de seleção de cores com a cor atual da linha como padrão.
        2. Verifica se a cor escolhida pelo usuário é válida.
        3. Atualiza a cor da linha com a cor escolhida.
        4. Atualiza a cor do botão de seleção de cor da linha para refletir a nova cor.

        Parâmetros:
        - Nenhum

        Retorno:
        - Nenhum retorno direto. A função atualiza a cor da linha e a cor do botão de seleção.

        Exceções tratadas:
        - Nenhuma exceção específica tratada nesta função.
        """

        # Abre o diálogo de seleção de cores com a cor atual da linha como padrão
        color = QColorDialog.getColor(self.line_color, self)

        # Verifica se a cor escolhida pelo usuário é válida
        if color.isValid():
            # Atualiza a cor da linha com a cor escolhida
            self.line_color = color

            # Atualiza a cor do botão de seleção de cor da linha para refletir a nova cor
            self.update_button_color(self.line_color_button, self.line_color)

    def choose_face_color(self):
        """
        Abre um diálogo de seleção de cores para escolher a cor das faces.
        Atualiza a cor do botão de seleção de cor das faces se uma cor válida for escolhida.

        Detalhamento:
        1. Abre o diálogo de seleção de cores com a cor atual das faces como padrão.
        2. Verifica se a cor escolhida pelo usuário é válida.
        3. Atualiza a cor das faces com a cor escolhida.
        4. Atualiza a cor do botão de seleção de cor das faces para refletir a nova cor.

        Parâmetros:
        - Nenhum

        Retorno:
        - Nenhum retorno direto. A função atualiza a cor das faces e a cor do botão de seleção.

        Exceções tratadas:
        - Nenhuma exceção específica tratada nesta função.
        """
        
        # Abre o diálogo de seleção de cores com a cor atual das faces como padrão
        color = QColorDialog.getColor(self.face_color, self)

        # Verifica se a cor escolhida pelo usuário é válida
        if color.isValid():
            # Atualiza a cor das faces com a cor escolhida
            self.face_color = color
            # Atualiza a cor do botão de seleção de cor das faces para refletir a nova cor
            self.update_button_color(self.face_color_button, self.face_color)

    def update_button_color(self, button, color):
        """
        Atualiza a cor de fundo de um botão para refletir a cor selecionada.

        Detalhamento:
        1. Obtém a paleta atual do botão.
        2. Define a nova cor de fundo do botão na paleta.
        3. Habilita o preenchimento automático de fundo do botão.
        4. Aplica a nova paleta ao botão.
        5. Atualiza o botão para refletir as mudanças.

        Parâmetros:
        - button: O botão cuja cor de fundo será atualizada.
        - color: A nova cor a ser aplicada ao botão.

        Retorno:
        - Nenhum retorno direto. A função atualiza a cor de fundo do botão.

        Exceções tratadas:
        - Nenhuma exceção específica tratada nesta função.
        """

        # Obtém a paleta atual do botão
        palette = button.palette()

        # Define a nova cor de fundo do botão na paleta
        palette.setColor(QPalette.ColorRole.Button, color)

        # Habilita o preenchimento automático de fundo do botão
        button.setAutoFillBackground(True)

        # Aplica a nova paleta ao botão
        button.setPalette(palette)

        # Atualiza o botão para refletir as mudanças
        button.update()

    def update_line_width_state(self):
        """
        Atualiza o estado de habilitação do spinbox da largura da linha com base no valor da opacidade da linha.

        Detalhamento:
        1. Verifica se o valor do spinbox de opacidade da linha é igual a 0.
        2. Se a opacidade da linha for 0, desativa o spinbox da largura da linha.
        3. Se a opacidade da linha for diferente de 0, ativa o spinbox da largura da linha.

        Parâmetros:
        - Nenhum

        Retorno:
        - Nenhum retorno direto. A função atualiza o estado de habilitação do spinbox da largura da linha.

        Exceções tratadas:
        - Nenhuma exceção específica tratada nesta função.
        """
        
        # Verifica se o valor do spinbox de opacidade da linha é igual a 0
        if self.line_opacity_spinbox.value() == 0:
            # Se a opacidade da linha for 0, desativa o spinbox da largura da linha
            self.line_width_spinbox.setEnabled(False)
        else:
            # Se a opacidade da linha for diferente de 0, ativa o spinbox da largura da linha
            self.line_width_spinbox.setEnabled(True)

    def get_style_options(self):
        """
        Retorna as opções de estilo definidas pelo usuário para a exportação KML.

        Detalhamento:
        1. Obtém o valor da largura da linha do spinbox correspondente.
        2. Converte o valor da opacidade da linha de percentual (0-100%) para uma escala de 0 a 255.
        3. Obtém a cor da linha selecionada pelo usuário.
        4. Converte o valor da opacidade das faces de percentual (0-100%) para uma escala de 0 a 255.
        5. Obtém a cor das faces selecionada pelo usuário.

        Parâmetros:
        - Nenhum

        Retorno:
        - Um dicionário contendo as opções de estilo:
            - "line_width": Largura da linha (float).
            - "line_opacity": Opacidade da linha (int, 0-255).
            - "line_color": Cor da linha (QColor).
            - "face_opacity": Opacidade das faces (int, 0-255).
            - "face_color": Cor das faces (QColor).

        Exceções tratadas:
        - Nenhuma exceção específica tratada nesta função.
        """
        
        # Retorna um dicionário com as opções de estilo
        return {
            "line_width": self.line_width_spinbox.value(),  # Obtém o valor da largura da linha
            "line_opacity": int(self.line_opacity_spinbox.value() * 2.55),  # Converte a opacidade da linha de % para 0-255
            "line_color": self.line_color,  # Obtém a cor da linha
            "face_opacity": int(self.face_opacity_spinbox.value() * 2.55),  # Converte a opacidade das faces de % para 0-255
            "face_color": self.face_color  # Obtém a cor das faces
        }

class ExportaMalha3D(QDialog):
    def __init__(self, parent=None):
        """
        Inicializa a interface de diálogo para exportação de malha 3D.

        Funções e Ações Desenvolvidas:
        - Configura o título da janela do diálogo.
        - Cria e organiza os layouts e botões do diálogo.
        - Aplica estilos personalizados aos botões.
        - Adiciona efeitos de sombra aos textos dos botões.

        :param parent: O widget pai do diálogo, se houver.
        """
        super().__init__(parent)
        
        self.setWindowTitle("Exportar Malha 3D")  # Define o título da janela do diálogo
        
        main_layout = QVBoxLayout()  # Cria o layout principal do tipo QVBoxLayout
        
        # Cria um frame para os botões com estilo de painel
        frame = QFrame()
        frame.setFrameShape(QFrame.Shape.StyledPanel)
        frame.setFrameShadow(QFrame.Shadow.Raised)
        
        frame_layout = QVBoxLayout()  # Cria o layout do frame do tipo QVBoxLayout
        
        # Layout superior com dois botões (DXF e DAE)
        top_layout = QHBoxLayout()
        self.button_dxf = QPushButton("DXF")
        self.button_dae = QPushButton("DAE")
        top_layout.addWidget(self.button_dxf)
        top_layout.addWidget(self.button_dae)
        
        # Layout inferior com dois botões (STL e OBJ)
        bottom_layout = QHBoxLayout()
        self.button_stl = QPushButton("STL")
        self.button_obj = QPushButton("OBJ")
        bottom_layout.addWidget(self.button_stl)
        bottom_layout.addWidget(self.button_obj)
        
        # Adiciona os layouts superior e inferior ao layout do frame
        frame_layout.addLayout(top_layout)
        frame_layout.addLayout(bottom_layout)
        frame.setLayout(frame_layout)  # Define o layout do frame
        
        # Botão de cancelar
        self.button_cancel = QPushButton("Cancelar")
        self.button_cancel.clicked.connect(self.reject)  # Conecta o botão cancelar à função reject
        
        # Adiciona o frame e o botão de cancelar ao layout principal
        main_layout.addWidget(frame)
        main_layout.addWidget(self.button_cancel)
        
        self.setLayout(main_layout)  # Define o layout principal do diálogo
        
        # Personalizar os botões
        self.estilizar_botoes()
        self.adicionar_sombra_nos_textos()

    def estilizar_botoes(self):
        """
        Aplica estilos personalizados aos botões de formato de exportação.

        Funções e Ações Desenvolvidas:
        - Define um estilo base para todos os botões.
        - Aplica estilos específicos para cada botão (DXF, DAE, STL, OBJ).
        """
        estilo_base = """
            QPushButton {
                border: 2px solid #8f8f91;
                border-radius: 5px;
                background-color: #f0f0f0;
                min-width: 70px;
                padding: 5px;
                font: bold 12px;
            }
            QPushButton:pressed {
                background-color: #e0e0e0;
            }
            QPushButton:hover {
                background-color: #d0d0d0;
            }
        """

        # Estilo personalizado para o botão DXF
        self.button_dxf.setStyleSheet(estilo_base + """
            QPushButton {
                border-color: #0078d7;
                color: #0078d7;
            }
            QPushButton:pressed {
                background-color: #005bb5;
                color: #ffffff;
            }
            QPushButton:hover {
                background-color: #3399ff;
            }
        """)

        # Estilo personalizado para o botão DAE
        self.button_dae.setStyleSheet(estilo_base + """
            QPushButton {
                border-color: #d70022;
                color: #d70022;
            }
            QPushButton:pressed {
                background-color: #b5001a;
                color: #ffffff;
            }
            QPushButton:hover {
                background-color: #ff3344;
            }
        """)

        # Estilo personalizado para o botão STL
        self.button_stl.setStyleSheet(estilo_base + """
            QPushButton {
                border-color: #008000;
                color: #008000;
            }
            QPushButton:pressed {
                background-color: #005500;
                color: #ffffff;
            }
            QPushButton:hover {
                background-color: #33cc33;
            }
        """)

        # Estilo personalizado para o botão OBJ
        self.button_obj.setStyleSheet(estilo_base + """
            QPushButton {
                border-color: #ffa500;
                color: #ffa500;
            }
            QPushButton:pressed {
                background-color: #cc8400;
                color: #ffffff;
            }
            QPushButton:hover {
                background-color: #ffc966;
            }
        """)

    def adicionar_sombra_nos_textos(self):
        """
        Adiciona efeitos de sombra aos textos dos botões de formato de exportação.

        Funções e Ações Desenvolvidas:
        - Cria um efeito de sombra para cada botão.
        - Aplica o efeito de sombra aos botões de formato de exportação.
        """
        botoes = [self.button_dxf, self.button_dae, self.button_stl, self.button_obj]  # Lista de botões

        # Itera sobre cada botão e aplica o efeito de sombra
        for botao in botoes:
            efeito_sombra = QGraphicsDropShadowEffect()
            efeito_sombra.setBlurRadius(10)  # Define o raio do desfoque da sombra
            efeito_sombra.setColor(QColor(0, 0, 0, 160))  # Define a cor da sombra com opacidade
            efeito_sombra.setOffset(2, 2)  # Define o deslocamento da sombra
            botao.setGraphicsEffect(efeito_sombra)  # Aplica o efeito de sombra ao botão

class CustomDelegate(QStyledItemDelegate):
    """
    CustomDelegate é uma classe que herda de QStyledItemDelegate.
    Esta classe é usada para personalizar a aparência de itens em um QTreeView, especificamente
    para desenhar ícones que representam camadas de malha no QGIS.
    
    Métodos:
    - __init__: Inicializa a classe CustomDelegate.
    - paint: Desenha o ícone personalizado para a camada de malha.
    - sizeHint: Define o tamanho dos itens no QTreeView.
    """
    def __init__(self, parent=None):
        """
        Inicializa a instância de CustomDelegate.

        Parâmetros:
        - parent: O pai do delegado, geralmente o QTreeView.
        """
        super(CustomDelegate, self).__init__(parent)

    def paint(self, painter, option, index):
        """
        Desenha o ícone personalizado para a camada de malha.

        Parâmetros:
        - painter: O QPainter usado para desenhar o ícone.
        - option: As opções de estilo usadas para desenhar o item.
        - index: O índice do item no modelo.
        """
        super(CustomDelegate, self).paint(painter, option, index)  # Chama o método paint da classe base
        layer_id = index.data(Qt.ItemDataRole.UserRole)
        layer = QgsProject.instance().mapLayer(layer_id)   # busca objeto
        if isinstance(layer, QgsMeshLayer):  # Verifica se a camada é do tipo QgsMeshLayer
            icon_size = 14  # Define o tamanho do ícone
            pixmap = QPixmap(icon_size, icon_size)  # Cria um QPixmap do tamanho do ícone
            pixmap.fill(Qt.GlobalColor.transparent)  # Preenche o QPixmap com transparência
            icon_painter = QPainter(pixmap)  # Cria um QPainter para desenhar no QPixmap
            icon_painter.setRenderHint(QPainter.RenderHint.Antialiasing)  # Habilita antialiasing para suavizar o desenho

            pen = QPen(Qt.GlobalColor.blue)  # Cria uma caneta com a cor azul
            pen.setWidth(1)  # Define a largura da caneta
            icon_painter.setPen(pen)  # Define a caneta no QPainter

            # Define points for the triangles
            points = [
                QPoint(0, 0), QPoint(icon_size // 2, 0), QPoint(icon_size - 1, 0),
                QPoint(0, icon_size // 2), QPoint(icon_size // 2, icon_size // 2), QPoint(icon_size - 1, icon_size // 2),
                QPoint(0, icon_size - 1), QPoint(icon_size // 2, icon_size - 1), QPoint(icon_size - 1, icon_size - 1)]

            # Define triangles with corresponding colors
            triangles = [
                (0, 1, 4, QColor(255, 255, 0)),  # Amarelo
                (0, 3, 4, QColor(255, 165, 0)),  # Laranja
                (1, 2, 4, QColor(255, 0, 255)),  # Roxo
                (3, 4, 7, QColor(255, 0, 255)),  # Roxo
                (3, 6, 7, QColor(255, 165, 0)),  # Laranja
                (4, 5, 8, QColor(255, 255, 0)),  # Amarelo
                (4, 7, 8, QColor(255, 0, 255))]   # Roxo

            # Draw the triangles with colors
            for t in triangles:
                icon_painter.setBrush(t[3])  # Define o pincel com a cor do triângulo
                icon_painter.drawPolygon(points[t[0]], points[t[1]], points[t[2]])  # Desenha o triângulo

            icon_painter.end()  # Finaliza o desenho

            icon = QIcon(pixmap)  # Cria um ícone a partir do QPixmap
            icon_rect = option.rect  # Obtém o retângulo de desenho
            icon_rect.setSize(QSize(icon_size, icon_size))  # Define o tamanho do retângulo do ícone
            icon_rect.moveLeft(option.rect.left() - 16)  # Move o ícone para a esquerda
            icon.paint(painter, icon_rect, Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft)  # Desenha o ícone no retângulo

    def sizeHint(self, option, index):
        """
        Define o tamanho dos itens no QTreeView.

        Parâmetros:
        - option: As opções de estilo usadas para desenhar o item.
        - index: O índice do item no modelo.

        Retorna:
        - QSize: O tamanho sugerido para o item.
        """
        size = super(CustomDelegate, self).sizeHint(option, index)  # Obtém o tamanho sugerido da classe base
        return QSize(size.width(), max(size.height(), 15))  # Retorna o tamanho ajustado