from qgis.core import QgsProject, QgsMessageLog, Qgis, QgsVectorLayer, QgsWkbTypes, QgsMapSettings, QgsMapRendererCustomPainterJob, QgsGeometry, QgsPointXY, QgsFeature, QgsLineSymbol, QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsField, QgsMarkerLineSymbolLayer, QgsSimpleMarkerSymbolLayer, QgsPalLayerSettings, QgsSymbol, QgsSimpleLineSymbolLayer, QgsMarkerSymbol, QgsTextFormat, QgsVectorLayerSimpleLabeling, QgsSymbolLayer, QgsUnitTypes, QgsEditorWidgetSetup, QgsDistanceArea, QgsEditFormConfig, QgsLayerTreeLayer, QgsLayerTreeGroup, QgsSingleSymbolRenderer, QgsProperty, QgsLabelLineSettings, QgsVector
from qgis.PyQt.QtWidgets import QDialog, QCheckBox, QColorDialog, QApplication, QListWidget, QLabel,QStyledItemDelegate, QStyle, QFileDialog, QPushButton
from PyQt5.QtGui import QImage, QPainter, QPixmap, QColor, QPen, QIntValidator, QFont, QBrush, QPalette, QStandardItemModel, QStandardItem
from qgis.PyQt.QtCore import Qt, QSize, QVariant, QRect, QPoint, QEvent, QSettings
from qgis.utils import iface
from qgis.PyQt import uic
import ezdxf
import math
import os

FORM_CLASS, _ = uic.loadUiType(os.path.join(os.path.dirname(__file__), 'Dimensionamento_Vetores.ui'))

class DimensionarVetores(QDialog, FORM_CLASS):
    def __init__(self, parent=None):
        """Constructor."""
        super(DimensionarVetores, self).__init__(parent)
        # Configura a interface do usuário a partir do Designer.
        self.setupUi(self)

        self.iface = iface

        # Altera o título da janela
        self.setWindowTitle("Dimensionamento de Vetores")

        self._widget_msg_sucesso = None   # referência ao último widget de sucesso
        self._dim_conn_layer     = None          # referência à camada de dimensões
        self._dim_conn_srcLayer  = None          # camada-origem que está “linkada”
        self._dim_conn_links     = {}            # {fid_origem: [fids_dimensão]}

        # depois de self.setupUi(self)
        self._tv_model = None
        self._tv_last_layer_ids = []
        self._tv_selected_layer_id = None
        self._last_layer_connected = None

        # ANGULAR CONNECTED
        self._ang_conn_layer      = None          # camada de arcos
        self._ang_conn_srcLayer   = None          # camada origem (polígono)
        self._ang_conn_layer_id   = ""            # id da camada angular
        self._ang_conn_links      = {}            # {fid_src: [fids_arc]}

        self._btns_last_src_layer = None  # para (des)conectar sinais de featureAdd/Del da camada do combo

        # configurar tree view
        self._init_treeview_camadas()

        self._update_buttons_state()

        # Preenche o comboBox com camadas de linha
        self.populate_combo_box()

        # Ao passar o mouse (hover) com um azul claro
        style = "QTreeView::item:hover { background: #cceeff; }"  # Azul claro no hover
        self.treeViewCamada.setStyleSheet(style)

        # Conecta os sinais aos slots
        self.connect_signals()

    def connect_signals(self):

       # Conecta a mudança de seleção no comboBoxCamada para atualizar o checkBoxSeleciona
        self.comboBoxCamada.currentIndexChanged.connect(self.update_checkBoxSeleciona)

        # Conecta também para atualizar os sinais da camada (selectionChanged)
        self.comboBoxCamada.currentIndexChanged.connect(self.update_layer_connections)

        # Conecta sinais do projeto para atualizar comboBox quando camadas forem adicionadas, removidas ou renomeadas
        QgsProject.instance().layersAdded.connect(self.populate_combo_box)
        QgsProject.instance().layersRemoved.connect(self.populate_combo_box)
        QgsProject.instance().layerWillBeRemoved.connect(self.populate_combo_box)

        # Conecta o botão pushButtonFechar
        self.pushButtonFechar.clicked.connect(self.close)

        self.pushButtonLivre.clicked.connect(self.criar_camada_dimensionamento)

        self.pushButtonConectada.clicked.connect(self.criar_camada_dimensionamento_conectada)

        # self.pushButtonAngular.clicked.connect(self.criar_camada_dimensionamento_angular)
        self.pushButtonAngular.clicked.connect(self.criar_camada_dimensionamento_angular_conectada)

        # Atualiza lista de camadas de dimensão ao mudar estrutura do projeto
        QgsProject.instance().layersAdded.connect(self.atualizar_treeViewCamadas)
        QgsProject.instance().layersRemoved.connect(self.atualizar_treeViewCamadas)
        QgsProject.instance().layerWillBeRemoved.connect(self.atualizar_treeViewCamadas)

        self.treeViewCamada.setItemDelegate(TreeDeleteButtonDelegate(self))

        self.pushButtonCor.clicked.connect(self._on_pick_color_for_layer)

        # NOVO: liga os 4 controles de estilo (aplica em tempo real na camada selecionada)
        for sb in (self.doubleSpinBoxTamanho,   # tamanho da seta (mm)
                   self.doubleSpinBoxAux,       # comprimento do símbolo auxiliar (tick) (mm)
                   self.doubleSpinBoxTexto,     # tamanho do texto (pt)
                   self.doubleSpinBoxRecuo):    # recuo da seta (mm)
            sb.valueChanged.connect(self._on_symbol_controls_changed)

        self.doubleSpinBoxTamanhoM.valueChanged.connect(self._on_min_length_changed_selected)
        self.checkBoxDirecao.stateChanged.connect(self._on_direction_changed_selected)

        self.pushButtonDXF.clicked.connect(self.exportar_dim_layer_para_dxf)

        self.comboBoxCamada.currentIndexChanged.connect(self._update_buttons_state)

        QgsProject.instance().layersAdded.connect(self._update_buttons_state)
        QgsProject.instance().layersRemoved.connect(self._update_buttons_state)
        QgsProject.instance().layerWillBeRemoved.connect(self._update_buttons_state)

    def showEvent(self, event):
        """
        Sobrescreve o evento de exibição do diálogo para resetar os Widgets.
        """
        super(DimensionarVetores, self).showEvent(event)

        # Reset do checkBoxSeleciona
        checkBox = self.findChild(QCheckBox, 'checkBoxSeleciona')
        if checkBox:
            checkBox.setChecked(False)
            checkBox.setEnabled(False)
        
        self.populate_combo_box()  # Atualiza o comboBoxCamada com as camadas disponíveis

        self.update_checkBoxSeleciona()  # Atualiza o estado do checkBoxSeleciona com base nas feições selecionadas

        self.update_layer_connections()  # Conecta os sinais da camada atual

        self.atualizar_treeViewCamadas()

        self._hook_src_layer_feature_signals()
        self._update_buttons_state()

    def mostrar_mensagem(self, texto, tipo, duracao=2, caminho_pasta=None, caminho_arquivo=None):
        """
        Exibe mensagem na barra do QGIS.
        tipo: 'Erro', 'Aviso' ou 'Sucesso'
        """
        bar = self.iface.messageBar()

        # remove eventual widget anterior de sucesso
        if tipo == "Sucesso" and self._widget_msg_sucesso is not None:
            try:
                bar.popWidget(self._widget_msg_sucesso)
            except Exception:
                bar.clearWidgets()
            self._widget_msg_sucesso = None

        if tipo == "Erro":
            bar.pushMessage("Erro", texto, level=Qgis.Critical, duration=duracao)

        elif tipo == "Aviso":
            bar.pushMessage("Aviso", texto, level=Qgis.Warning, duration=duracao)

        elif tipo == "Sucesso":
            msg = bar.createMessage("Sucesso", texto)

            # botão para abrir pasta (opcional)
            if caminho_pasta:
                btn_pasta = QPushButton("Abrir Pasta")
                btn_pasta.clicked.connect(lambda: os.startfile(caminho_pasta))
                msg.layout().insertWidget(1, btn_pasta)

            # botão para abrir arquivo (opcional)
            if caminho_arquivo:
                btn_exec = QPushButton("Abrir Arquivo")
                btn_exec.clicked.connect(lambda: os.startfile(caminho_arquivo))
                msg.layout().insertWidget(2, btn_exec)

            # mostra o widget e guarda referência
            self._widget_msg_sucesso = bar.pushWidget(msg, level=Qgis.Info, duration=duracao)

    def update_checkBoxSeleciona(self):
        """
        Atualiza o estado do checkBoxSeleciona com base na seleção de feições da camada atualmente selecionada.

        Parâmetros:
        self : objeto
            Referência à instância atual da classe.

        A função realiza as seguintes ações:
        - Obtém o ID da camada atualmente selecionada no comboBoxCamada.
        - Se uma camada válida for encontrada, verifica a quantidade de feições selecionadas na camada.
        - Se houver feições selecionadas, o checkBoxSeleciona é ativado.
        - Se não houver feições selecionadas ou a camada não for válida, o checkBoxSeleciona é desativado e desmarcado.
        """
        layer_id = self.comboBoxCamada.currentData()  # Obtém o ID da camada selecionada no comboBoxCamada
        if layer_id:  # Verifica se há uma camada selecionada
            layer = QgsProject.instance().mapLayer(layer_id)  # Obtém a camada correspondente ao ID
            if layer:  # Verifica se a camada existe
                selected_features = layer.selectedFeatureCount()  # Conta o número de feições selecionadas na camada
                if selected_features > 0:  # Se houver feições selecionadas, ativa o checkBoxSeleciona
                    self.findChild(QCheckBox, 'checkBoxSeleciona').setEnabled(True)
                else:  # Se não houver feições selecionadas, desativa o checkBoxSeleciona e o desmarca
                    self.findChild(QCheckBox, 'checkBoxSeleciona').setEnabled(False)
                    self.findChild(QCheckBox, 'checkBoxSeleciona').setChecked(False)
            else:  # Se a camada não for válida, desativa o checkBoxSeleciona e o desmarca
                self.findChild(QCheckBox, 'checkBoxSeleciona').setEnabled(False)
                self.findChild(QCheckBox, 'checkBoxSeleciona').setChecked(False)
        else:  # Se não houver uma camada selecionada, desativa o checkBoxSeleciona e o desmarca
            self.findChild(QCheckBox, 'checkBoxSeleciona').setEnabled(False)
            self.findChild(QCheckBox, 'checkBoxSeleciona').setChecked(False)

    def update_layer_connections(self):
        """
        Conecta o sinal selectionChanged da camada selecionada no comboBoxCamada à função update_checkBoxSeleciona,
        e atualiza o estado do checkBoxSeleciona imediatamente.

        Parâmetros:
        self : objeto
            Referência à instância atual da classe.

        A função realiza as seguintes ações:
        - Obtém o ID da camada atualmente selecionada no comboBoxCamada.
        - Se uma camada válida for encontrada, conecta o sinal selectionChanged da camada à função update_checkBoxSeleciona.
        - Atualiza imediatamente o estado do checkBoxSeleciona com base na seleção de feições.
        - Se não houver uma camada selecionada, desativa o checkBoxSeleciona.
        """
        layer_id = self.comboBoxCamada.currentData()
        # desconecta da anterior
        try:
            if self._last_layer_connected is not None:
                self._last_layer_connected.selectionChanged.disconnect(self.update_checkBoxSeleciona)
        except Exception:
            pass

        if layer_id:
            layer = QgsProject.instance().mapLayer(layer_id)
            if layer:
                try:
                    layer.selectionChanged.connect(self.update_checkBoxSeleciona)
                except Exception:
                    pass
                self._last_layer_connected = layer
                self.update_checkBoxSeleciona()
                return

        self._last_layer_connected = None

        self._hook_src_layer_feature_signals()
        self._update_buttons_state()

        self.update_checkBoxSeleciona()

    def populate_combo_box(self):
        """
        Popula o comboBoxCamada com as camadas de polígonos disponíveis no projeto e realiza ações relacionadas.

        Parâmetros:
        self : objeto
            Referência à instância atual da classe.

        A função realiza as seguintes ações:
        - Salva a camada atualmente selecionada no comboBoxCamada.
        - Bloqueia temporariamente os sinais do comboBoxCamada para evitar atualizações desnecessárias.
        - Limpa o comboBoxCamada antes de preenchê-lo novamente.
        - Adiciona as camadas de polígonos disponíveis ao comboBoxCamada.
        - Restaura a seleção da camada anterior, se possível.
        - Desbloqueia os sinais do comboBoxCamada após preenchê-lo.
        - Preenche o comboBoxRotulagem com os campos da camada selecionada.
        - Ativa ou desativa o botão pushButtonConverter com base na presença de camadas no comboBoxCamada.
        """
        current_layer_id = self.comboBoxCamada.currentData()  # Salva a camada atualmente selecionada
        self.comboBoxCamada.blockSignals(True)  # Evita disparar eventos desnecessários
        self.comboBoxCamada.clear()

        layer_list = QgsProject.instance().mapLayers().values()
        polygon_layers = [
            layer for layer in layer_list 
            if isinstance(layer, QgsVectorLayer) and QgsWkbTypes.geometryType(layer.wkbType()) == QgsWkbTypes.PolygonGeometry]

        for layer in polygon_layers:
            self.comboBoxCamada.addItem(layer.name(), layer.id())
            layer.nameChanged.connect(self.update_combo_box_item)  # Mantém a conexão para atualizar nomes

        # Restaura a camada anteriormente selecionada, se ainda existir
        if current_layer_id:
            index = self.comboBoxCamada.findData(current_layer_id)
            if index != -1:
                self.comboBoxCamada.setCurrentIndex(index)

        self.comboBoxCamada.blockSignals(False)  # Libera os sinais
        
        # Reconecta os sinais da camada atual, garantindo que o selectionChanged esteja conectado
        self.update_layer_connections()

        # Reconecta sinais da camada atual + atualiza botões
        self.update_layer_connections()
        self._hook_src_layer_feature_signals()
        self._update_buttons_state()

    def update_combo_box_item(self):
        """
        Atualiza o texto dos itens no comboBoxCamada com base nos nomes atuais das camadas no projeto.

        Parâmetros:
        self : objeto
            Referência à instância atual da classe.

        A função realiza as seguintes ações:
        - Itera sobre os itens no comboBoxCamada.
        - Para cada item, obtém o ID da camada correspondente.
        - Atualiza o nome exibido no comboBoxCamada com o nome atual da camada, caso a camada ainda exista.
        - Atualiza o campo de nome do polígono (lineEditNome) após atualizar o comboBox.
        """
        
        for i in range(self.comboBoxCamada.count()):  # Itera sobre todos os itens no comboBoxCamada
            layer_id = self.comboBoxCamada.itemData(i)  # Obtém o ID da camada para o item atual
            layer = QgsProject.instance().mapLayer(layer_id)  # Obtém a camada correspondente ao ID
            if layer:  # Verifica se a camada existe
                self.comboBoxCamada.setItemText(i, layer.name())  # Atualiza o texto do item com o nome atual da camada

    def _hook_src_layer_feature_signals(self):
        """(Re)conecta featureAdded/featureDeleted da camada atualmente selecionada no combo,
        para atualizar os botões em tempo real quando o número de feições mudar."""
        # desconecta da anterior
        try:
            if isinstance(self._btns_last_src_layer, QgsVectorLayer):
                self._btns_last_src_layer.featureAdded.disconnect(self._on_src_layer_feature_changed)
                self._btns_last_src_layer.featureDeleted.disconnect(self._on_src_layer_feature_changed)
        except Exception:
            pass

        self._btns_last_src_layer = None

        # conecta na atual
        layer_id = self.comboBoxCamada.currentData()
        if not layer_id:
            return
        lyr = QgsProject.instance().mapLayer(layer_id)
        if isinstance(lyr, QgsVectorLayer):
            try:
                lyr.featureAdded.connect(self._on_src_layer_feature_changed)
                lyr.featureDeleted.connect(self._on_src_layer_feature_changed)
                self._btns_last_src_layer = lyr
            except Exception:
                pass

    def _on_src_layer_feature_changed(self, *args):
        """Reavalia e atualiza o estado dos botões quando ocorre adição ou exclusão de feições na camada de origem."""
        # qualquer mudança de feições → reavalia botões
        self._update_buttons_state()

    def _update_buttons_state(self):
        """Habilita/Desabilita botões conforme:
           - Conectada/Angular: precisa haver camada no combo E a camada ter feições (>0).
           - Cor/DXF: precisa haver itens no treeViewCamada.
           Atualiza em 'tempo real' pois é chamado por vários sinais."""
        # --- estado do combo ---
        has_combo_layers = self.comboBoxCamada.count() > 0
        layer_id = self.comboBoxCamada.currentData()
        lyr = QgsProject.instance().mapLayer(layer_id) if layer_id else None
        has_features = False
        try:
            if isinstance(lyr, QgsVectorLayer):
                # featureCount() é barato o suficiente aqui
                has_features = (lyr.featureCount() or 0) > 0
        except Exception:
            has_features = False

        # --- estado do tree ---
        has_tree_items = False
        try:
            has_tree_items = (self._tv_model.rowCount() > 0)
        except Exception:
            pass

        # --- aplica nos botões (usa getattr para não quebrar se algum não existir) ---
        for btn_name, enabled in (
            ("pushButtonConectada", has_combo_layers and has_features),
            ("pushButtonAngular",   has_combo_layers and has_features),
            ("pushButtonCor",       has_tree_items),
            ("pushButtonDXF",       has_tree_items)):
            btn = getattr(self, btn_name, None)
            if btn is not None:
                btn.setEnabled(bool(enabled))

    def _gerar_nome_unico(self, nome_base):
        """Gera nome único para camada, evitando duplicidade no projeto."""
        layers_names = [layer.name() for layer in QgsProject.instance().mapLayers().values()]
        nome = nome_base
        contador = 1
        while nome in layers_names:
            nome = f"{nome_base}_{contador}"
            contador += 1
        return nome

    def closeEvent(self, event):
        """Desconecta atualização automática ao fechar o diálogo."""
        super().closeEvent(event)

    def configurar_campo_oculto(self, camada: QgsVectorLayer, nomes: list[str]):
        """
        Oculta os campos dados tanto na tabela de atributos quanto no formulário de edição.
        """
        for nome in nomes:
            idx = camada.fields().indexOf(nome)
            if idx != -1:
                setup = QgsEditorWidgetSetup("Hidden", {})
                camada.setEditorWidgetSetup(idx, setup)

    def atualizar_dimensao_atribs(self, camada: QgsVectorLayer, fid: int):
        """
        Calcula o comprimento da feição 'fid', grava nos campos
        'comprimento' (numérico) e 'label' (string).
        """
        f = camada.getFeature(fid)

        # Comprimento em unidades do layer/project
        da = QgsDistanceArea()
        if camada.crs().isGeographic():
            da.setSourceCrs(camada.crs(), QgsProject.instance().transformContext())
            da.setEllipsoid(QgsProject.instance().crs().ellipsoidAcronym())
            comp = da.measureLength(f.geometry())      # metros
        else:
            comp = f.geometry().length()               # mesma unidade do layer

        comp = round(comp, 3)                          # 3 casas

        # Índices de campos
        idx_comp = camada.fields().indexOf("comprimento")
        idx_lab  = camada.fields().indexOf("label")

        # Muda valores
        camada.changeAttributeValue(fid, idx_comp, comp)
        camada.changeAttributeValue(fid, idx_lab, f"{comp:.2f} m")

        # Etiqueta é configurada uma única vez
        if not getattr(camada, "etiquetas_configuradas", False):
            self.configurar_etiquetas(camada)
            camada.etiquetas_configuradas = True

    def _get_rendered_offset_mu(self, layer: QgsVectorLayer, default_val: float = 0.0) -> float:
        """Obtém o valor de offset (afastamento em map units) aplicado ao símbolo da camada, com fallback padrão."""
        try:
            r = layer.renderer()
            if r:
                sym = r.symbol()
                if sym and sym.symbolLayerCount() > 0:
                    sll0 = sym.symbolLayer(0)
                    if hasattr(sll0, "offset"):
                        return float(sll0.offset())   # valor com sinal
        except Exception:
            pass
        return float(default_val)

    def configurar_etiquetas(self, camada: QgsVectorLayer, sinal_lado: int | None = None, distancia_mu: float = 0.0):
        """Configura os rótulos da camada de dimensionamento para exibir o campo 'label', alinhados à linha, no mesmo lado e afastamento definidos pelo símbolo."""
        pal = QgsPalLayerSettings()
        pal.fieldName = 'label'
        pal.placement = QgsPalLayerSettings.Line

        # >>> lado/distância vindos do offset real do símbolo
        off = self._get_rendered_offset_mu(camada, default_val=(distancia_mu * (1 if (sinal_lado or 1) > 0 else -1)))
        pal.dist = abs(off)
        pal.distUnits = QgsUnitTypes.RenderMapUnits
        pal.placementFlags = (QgsPalLayerSettings.BelowLine if off >= 0 else QgsPalLayerSettings.AboveLine)

        fmt = QgsTextFormat()
        fmt.setSize(8)
        fmt.setColor(QColor("black"))
        pal.setFormat(fmt)

        camada.setLabeling(QgsVectorLayerSimpleLabeling(pal))
        camada.setLabelsEnabled(True)

    def criar_camada_dimensionamento(self):
        """
        Cria uma camada de dimensionamento do tipo 'Livre',
        permitindo que o usuário insira manualmente as linhas de cota.
        A camada é adicionada ao grupo 'Dimensionamento' e entra em modo edição.
        """
        crs = self.iface.mapCanvas().mapSettings().destinationCrs()
        # Nenhuma feição ainda — camada vazia mas editável
        layer = self._criar_camada_dim_base("Dim_Livre", crs, [], editavel=True)

        # Suprime o formulário de edição
        cfg = layer.editFormConfig()
        cfg.setSuppress(QgsEditFormConfig.SuppressOn)
        layer.setEditFormConfig(cfg)

        # Atualiza atributos ao adicionar feições
        layer.featureAdded.connect(lambda fid: self.atualizar_dimensao_atribs(layer, fid))

        # >>> NOVO: deixar ativa, entrar em edição e ligar a ferramenta de captura <<<
        try:
            # 1) torna a nova camada a camada ativa
            self.iface.setActiveLayer(layer)
        except Exception:
            pass

        # 2) garante modo de edição
        if not layer.isEditable():
            layer.startEditing()

        # 3) ativa a ferramenta de digitalizar (adicionar feição de linha)
        #    Em QGIS 3.x, actionAddFeature usa o tipo da camada ativa.
        try:
            self.iface.actionAddFeature().trigger()
        except Exception:
            # fallback para builds que ainda expõem ações específicas por geometria
            try:
                self.iface.actionAddPolyline().trigger()
            except Exception:
                pass

        self.mostrar_mensagem("Camada de dimensionamento criada com sucesso (edição e digitalização ativadas).", "Sucesso")
        self.layer_dimensoes = layer

        self.atualizar_treeViewCamadas()  # atualiza a lista

    def _dim_on_geom_changed(self, fid, new_geom):
        """Atualiza as dimensões ligadas quando a geometria de uma feição da camada de origem é modificada."""
        layer = self._dim_layer()
        if not layer or not self._dim_conn_srcLayer:
            return

        layer.setReadOnly(False)     # 🔓 destrava

        prov = layer.dataProvider()
        prov.deleteFeatures(self._dim_conn_links.get(fid, []))

        feat = self._dim_conn_srcLayer.getFeature(fid)
        min_len = self.doubleSpinBoxTamanhoM.value()
        feats_dim = self._gera_dimensoes_para_feat(feat, layer.crs(), comprimento_min=min_len)

        ok, feats_dim = prov.addFeatures(feats_dim)
        self._dim_conn_links[fid] = [f.id() for f in feats_dim]

        layer.triggerRepaint()
        layer.setReadOnly(True)      # 🔒 trava de novo

    def _dim_on_feat_added(self, fid):
        """Cria e adiciona dimensões correspondentes quando uma nova feição é inserida na camada de origem."""
        layer = self._dim_layer()
        if not layer:
            return

        # Guarda contra duplicação: se geometryChanged já gerou as dimensões, não refaz
        if fid in self._dim_conn_links and self._dim_conn_links.get(fid):
            return

        prov = layer.dataProvider()  # provedor da camada de dimensões
        feat = self._dim_conn_srcLayer.getFeature(fid)  # feição recém adicionada da camada de origem
        min_len = self.doubleSpinBoxTamanhoM.value()  # comprimento mínimo configurado no spinBox

        # gera as linhas de dimensão para a feição adicionada
        feats_dim = self._gera_dimensoes_para_feat(feat, layer.crs(), comprimento_min=min_len)

        # adiciona as dimensões à camada
        ok, feats_dim = prov.addFeatures(feats_dim)

        # registra os FIDs das dimensões criadas, vinculando ao fid da feição origem
        self._dim_conn_links[fid] = [f.id() for f in feats_dim]

        layer.triggerRepaint()  # força o redesenho da camada

    def _dim_layer(self):
        """Retorna a camada de dimensão conectada ou None se não existir mais."""
        return QgsProject.instance().mapLayer(getattr(self, "_dim_conn_layer_id", ""))

    def _dim_on_feat_deleted(self, fid):
        """Remove as dimensões associadas quando uma feição é deletada da camada de origem."""
        layer = self._dim_layer()
        if not layer:
            return

        prov = layer.dataProvider()
        prov.deleteFeatures(self._dim_conn_links.pop(fid, []))

        layer.triggerRepaint()

    def criar_camada_dimensionamento_conectada(self):
        """
        Gera dimensões conectadas às feições de uma camada poligonal.
        Se checkBoxSeleciona marcado → só feições selecionadas, senão todas.
        Dimensões acompanham alterações em tempo real.
        """
        layer_id = self.comboBoxCamada.currentData()
        if not layer_id:
            self.mostrar_mensagem("Selecione uma camada de polígono.", "Erro")
            return
        src_layer = QgsProject.instance().mapLayer(layer_id)

        # Escolhe feições origem
        chk = self.findChild(QCheckBox, 'checkBoxSeleciona')
        feats_src = list(src_layer.getSelectedFeatures()) if chk and chk.isChecked() else list(src_layer.getFeatures())
        if not feats_src:
            self.mostrar_mensagem("Nenhuma feição encontrada para dimensionar.", "Erro")
            return

        crs = src_layer.crs()

        # Cria camada base (não editável)
        layer = self._criar_camada_dim_base("Dim_Conectada", crs, [], editavel=False)

        # marca como conectada e guarda a origem
        layer.setCustomProperty("dim/connected", True)
        layer.setCustomProperty("dim/src_layer_id", src_layer.id())

        self._dim_conn_layer = layer

        layer.setReadOnly(True)          # trava p/ o usuário

        self._dim_conn_layer_id = layer.id()  # ✅ Aqui sim é válido!
        self._dim_conn_srcLayer = src_layer

        # Gera dimensões iniciais
        prov = self._dim_conn_layer.dataProvider()
        for feat in feats_src:
            # feats_dim = self._gera_dimensoes_para_feat(feat, crs)
            min_len = self.doubleSpinBoxTamanhoM.value()
            feats_dim = self._gera_dimensoes_para_feat(feat, layer.crs(), comprimento_min=min_len)

            ok, feats_dim = prov.addFeatures(feats_dim)   # ← pega Features já com FIDs reais
            self._dim_conn_links[feat.id()] = [f.id() for f in feats_dim]

        self._dim_conn_layer.triggerRepaint()

        # Conecta sinais dinâmicos
        src_layer.geometryChanged.connect(self._dim_on_geom_changed)
        src_layer.featureAdded.connect(self._dim_on_feat_added)
        src_layer.featureDeleted.connect(self._dim_on_feat_deleted)

        self.mostrar_mensagem("Dimensões conectadas dinâmicas criadas.", "Sucesso")
        self.layer_dimensoes = self._dim_conn_layer

        self.atualizar_treeViewCamadas() # Atualiza a lista

    def _sync_tree_item_name(self, layer_id: str):
        """Atualiza o texto do item do tree para refletir o nome atual da camada."""
        item  = self._find_tree_item_by_layer_id(layer_id)
        if not item:
            return
        layer = QgsProject.instance().mapLayer(layer_id)
        if not layer:
            return
        item.setText(layer.name())

    def atualizar_dimensao_conectada(self):
        """
        Atualiza a camada de dimensão conectada dinamicamente, com base nos novos parâmetros
        (ex: comprimento mínimo ou direção).
        """
        if not self._dim_conn_layer or not self._dim_conn_srcLayer:
            return

        # Desconecta sinais antigos temporariamente
        try:
            self._dim_conn_srcLayer.geometryChanged.disconnect(self._dim_on_geom_changed)
            self._dim_conn_srcLayer.featureAdded.disconnect(self._dim_on_feat_added)
            self._dim_conn_srcLayer.featureDeleted.disconnect(self._dim_on_feat_deleted)
        except Exception:
            pass

        # Remove camada de dimensão anterior
        QgsProject.instance().removeMapLayer(self._dim_conn_layer.id())
        self._dim_conn_layer = None
        self._dim_conn_layer_id = ""
        self._dim_conn_links.clear()

        # Gera nova camada com os novos parâmetros
        self.criar_camada_dimensionamento_conectada()

        return

    def _gera_dimensoes_para_feat(self, feat: QgsFeature, crs: QgsCoordinateReferenceSystem, comprimento_min: float = 0.0) -> list[QgsFeature]:
        """
        Cria linhas de dimensão.
        - Segmentos menores que comprimento_min são sempre agregados ao vizinho,
          inclusive se ficarem no final do anel (garante cobertura sem “buracos”).
        """
        geom = feat.geometry()
        if not geom or geom.isEmpty() or not geom.isGeosValid():
            return []

        da = QgsDistanceArea()
        da.setSourceCrs(crs, QgsProject.instance().transformContext())
        da.setEllipsoid(QgsProject.instance().crs().ellipsoidAcronym())

        parts = geom.asMultiPolygon() if geom.isMultipart() else [geom.asPolygon()]
        feats_dim = []

        for part in parts:
            for ring in part:
                # percorre todos os lados do anel
                comp_acum = 0.0
                p0 = ring[0]            # ponto de início do trecho em acumulação
                last_fid = None         # índice do último feature criado
                for i in range(len(ring) - 1):
                    p1 = ring[i]
                    p2 = ring[i + 1]
                    seg_len = (da.measureLength(QgsGeometry.fromPolylineXY([QgsPointXY(p1), QgsPointXY(p2)]))
                                if crs.isGeographic() else
                                QgsGeometry.fromPolylineXY([QgsPointXY(p1), QgsPointXY(p2)]).length())

                    comp_acum += seg_len

                    # quando ultrapassar limite, cria/atualiza feature
                    if comp_acum >= comprimento_min:
                        f = QgsFeature()
                        f.setGeometry(QgsGeometry.fromPolylineXY([QgsPointXY(p0), QgsPointXY(p2)]))
                        f.setAttributes([round(comp_acum, 3), f"{comp_acum:.2f} m"])
                        feats_dim.append(f)
                        last_fid = len(feats_dim) - 1
                        # reinicia acumulação
                        comp_acum = 0.0
                        p0 = p2

                # Se sobrou trecho < min no final, agrega à última linha criada
                if comp_acum > 0 and last_fid is not None:
                    # estende a geometria e o comprimento do último feature
                    f_last = feats_dim[last_fid]
                    g_last = f_last.geometry().asPolyline()
                    g_last[-1] = QgsPointXY(ring[-1])       # ajusta ponto final
                    f_last.setGeometry(QgsGeometry.fromPolylineXY(g_last))
                    total = f_last.attribute(0) + comp_acum
                    f_last.setAttributes([round(total, 3), f"{total:.2f} m"])

        return feats_dim

    def _init_treeview_camadas(self):
        """Cria o modelo e configura o treeViewCamada."""
        model = QStandardItemModel(0, 1, self)  # 1 coluna
        model.setHeaderData(0, Qt.Horizontal, "Dimensionamentos")
        self._tv_model = model

        # Centraliza o cabeçalho
        header = self.treeViewCamada.header()
        header.setDefaultAlignment(Qt.AlignCenter)

        self.treeViewCamada.setModel(model)
        self.treeViewCamada.setHeaderHidden(False)
        self.treeViewCamada.setRootIsDecorated(False)  # sem ícones de expandir
        self.treeViewCamada.setAllColumnsShowFocus(True)
        self.treeViewCamada.setItemsExpandable(False)
        self.treeViewCamada.setExpandsOnDoubleClick(False)
        self.treeViewCamada.setSelectionBehavior(self.treeViewCamada.SelectRows)
        self.treeViewCamada.setSelectionMode(self.treeViewCamada.SingleSelection)

        # delegate do “X”
        self.treeViewCamada.setItemDelegate(TreeDeleteButtonDelegate(self))

        # conecta seleção
        try:
            self.treeViewCamada.selectionModel().selectionChanged.connect(self._on_tv_selection_changed)
        except Exception:
            pass  # se ainda não houver selectionModel, a próxima chamada a atualizar resolverá

        try:
            self._tv_model.rowsInserted.connect(lambda *args: self._update_buttons_state())
            self._tv_model.rowsRemoved.connect(lambda *args: self._update_buttons_state())
            self._tv_model.modelReset.connect(lambda *args: self._update_buttons_state())
        except Exception:
            pass

    def atualizar_treeViewCamadas(self):
        """
        Preenche o treeViewCamada com as camadas do grupo 'Dimensionamento'.
        Regras:
          - Texto em negrito;
          - Garante sempre 1 selecionada, se houver itens;
          - Se nova camada foi adicionada, seleciona a nova;
          - Se deletada, seleciona a anterior (ou última disponível);
          - Se seleção anterior ainda existir, preserva.
        """
        if self._tv_model is None:
            self._init_treeview_camadas()

        model = self._tv_model
        view  = self.treeViewCamada

        # snapshot anterior
        old_ids = list(self._tv_last_layer_ids)
        prev_selected_id = self._tv_selected_layer_id

        # limpa modelo
        model.removeRows(0, model.rowCount())

        # coleta camadas do grupo
        root = QgsProject.instance().layerTreeRoot()
        grupo_dim = root.findGroup("Dimensionamento")

        new_ids = []
        if grupo_dim:
            for node in grupo_dim.findLayers():
                lyr = node.layer()
                if isinstance(lyr, QgsVectorLayer) and lyr.geometryType() == QgsWkbTypes.LineGeometry:
                    it = QStandardItem(lyr.name())
                    f  = it.font()
                    f.setBold(True)
                    it.setFont(f)
                    it.setEditable(False)
                    it.setData(lyr.id(), Qt.UserRole)
                    model.appendRow(it)

                    # mantém o nome sincronizado em tempo real
                    try:
                        lyr.nameChanged.connect(lambda lid=lyr.id(): self._sync_tree_item_name(lid))
                    except Exception:
                        pass

                    # pinta o texto do item com a cor atual da camada
                    try:
                        it.setForeground(QBrush(self._get_layer_main_color(lyr)))
                    except Exception:
                        pass

                    # mantém sincronizado se o estilo/cor da camada mudar
                    try:
                        lyr.rendererChanged.connect(lambda lid=lyr.id(): self._sync_tree_item_color(lid))
                    except Exception:
                        pass
                    try:
                        lyr.styleChanged.connect(lambda lid=lyr.id(): self._sync_tree_item_color(lid))
                    except Exception:
                        pass

                    new_ids.append(lyr.id())

        # guarda nova ordem
        self._tv_last_layer_ids = list(new_ids)

        # --- atualiza estado dos botões já após popular a lista (importante para 1 item) ---
        self._update_buttons_state()

        # helper para selecionar por id
        def _select_id(layer_id: str) -> bool:
            for row in range(model.rowCount()):
                idx = model.index(row, 0)
                if model.data(idx, Qt.UserRole) == layer_id:
                    view.setCurrentIndex(idx)
                    self._tv_selected_layer_id = layer_id
                    return True
            return False

        # sem camadas → limpa seleção
        if not new_ids:
            view.clearSelection()
            self._tv_selected_layer_id = None
            self._update_buttons_state()  # <- garante desabilitar quando zera
            return

        # diffs
        added_ids   = [i for i in new_ids if i not in old_ids]
        removed_ids = [i for i in old_ids if i not in new_ids]

        # 1) se houve adição → seleciona a última adicionada
        if added_ids and _select_id(added_ids[-1]):
            self._update_buttons_state()  # <- atualiza antes de sair
            return

        # 2) preserva seleção anterior se ela ainda existir
        if prev_selected_id and prev_selected_id in new_ids and _select_id(prev_selected_id):
            self._update_buttons_state()  # <- atualiza antes de sair
            return

        # 3) se houve remoção → seleciona a "anterior" aproximada
        if removed_ids:
            try:
                removed_pos = min(old_ids.index(r) for r in removed_ids if r in old_ids)
            except ValueError:
                removed_pos = 0
            new_index = min(removed_pos, len(new_ids) - 1)
            if _select_id(new_ids[new_index]):
                self._update_buttons_state()  # <- atualiza antes de sair
                return

        # 4) fallback: seleciona a primeira
        _select_id(new_ids[0])

        # após resolver seleção/fallback
        self._update_buttons_state()

    def _get_layer_main_color(self, layer: QgsVectorLayer) -> QColor:
        """Tenta ler a cor 'principal' do símbolo (fallback: preto)."""
        try:
            r = layer.renderer()
            if not r:
                return QColor("black")
            sym = r.symbol()
            if not sym or sym.symbolLayerCount() == 0:
                return QColor("black")
            sll0 = sym.symbolLayer(0)
            if hasattr(sll0, "color"):
                c = sll0.color()
                if isinstance(c, QColor) and c.isValid():
                    return c
            if hasattr(sll0, "outlineColor"):
                c = sll0.outlineColor()
                if isinstance(c, QColor) and c.isValid():
                    return c
        except Exception:
            pass
        return QColor("black")

    def _find_tree_item_by_layer_id(self, layer_id: str) -> QStandardItem | None:
        """Retorna o item do treeViewCamada correspondente ao ID da camada, ou None se não encontrado."""
        if not self._tv_model:
            return None
        for row in range(self._tv_model.rowCount()):
            idx = self._tv_model.index(row, 0)
            if self._tv_model.data(idx, Qt.UserRole) == layer_id:
                return self._tv_model.itemFromIndex(idx)
        return None

    def _sync_tree_item_color(self, layer_id: str):
        """Atualiza a cor do texto do item do tree para refletir a cor da camada."""
        layer = QgsProject.instance().mapLayer(layer_id)
        item  = self._find_tree_item_by_layer_id(layer_id)
        if not layer or not item:
            return
        col = self._get_layer_main_color(layer)
        item.setForeground(QBrush(col))

    def _on_pick_color_for_layer(self):
        """
        Abre o seletor de cor e aplica a cor escolhida:
          - ao nome da camada (item do treeViewCamada);
          - ao símbolo da camada de dimensão (linha, setas e tick).
        Atualiza em tempo real.
        """
        # índice selecionado no treeView
        view = self.treeViewCamada
        sel = view.selectionModel().selectedIndexes()
        if not sel:
            self.mostrar_mensagem("Selecione uma camada de dimensão na lista.", "Aviso")
            return
        idx = sel[0]
        layer_id = self._tv_model.data(idx, Qt.UserRole)
        layer = QgsProject.instance().mapLayer(layer_id)
        if not isinstance(layer, QgsVectorLayer) or layer.geometryType() != QgsWkbTypes.LineGeometry:
            self.mostrar_mensagem("A camada selecionada não é uma camada de linhas de dimensionamento.", "Erro")
            return

        # cor inicial (tenta pegar do símbolo base)
        initial = QColor("black")
        try:
            sym = layer.renderer().symbol()
            if sym and sym.symbolLayerCount() > 0:
                sll0 = sym.symbolLayer(0)
                if hasattr(sll0, "color"):
                    initial = sll0.color()
                elif hasattr(sll0, "outlineColor"):
                    initial = sll0.outlineColor()
        except Exception:
            pass

        color = QColorDialog.getColor(initial, self, "Escolha a cor da dimensão")
        if not color.isValid():
            return

        # pinta item do treeview
        item = self._tv_model.itemFromIndex(idx)
        if item:
            item.setForeground(QBrush(color))

        # aplica cor no símbolo da camada
        self._apply_dim_color(layer, color)

        # Monitora a atualização do dimensionamento
        self._sync_tree_item_color(layer.id())

        # refresh visual
        layer.triggerRepaint()
        try:
            self.iface.mapCanvas().refresh()
        except Exception:
            pass

    def _apply_dim_color(self, layer: QgsVectorLayer, color: QColor):
        """
        Aplica 'color' às partes da camada de dimensão de forma SEGURA:
          - pausa renderização do canvas;
          - clona o símbolo e sub-símbolos antes de editar;
          - seta um renderer novo na camada;
          - retoma renderização.
        """
        if not layer or not layer.renderer():
            return

        canvas = None
        try:
            canvas = self.iface.mapCanvas()
            if canvas:
                canvas.setRenderFlag(False)  # ⏸️ pausa render
        except Exception:
            canvas = None

        try:
            # 1) Clona o símbolo raiz (não muta o símbolo em uso pelo render)
            sym_old = layer.renderer().symbol()
            if sym_old is None:
                return
            sym = sym_old.clone()

            # 2) Edita as symbol layers do clone
            count = sym.symbolLayerCount()
            for i in range(count):
                sll = sym.symbolLayer(i)

                # Linha principal: tenta setColor se disponível
                if hasattr(sll, "setColor"):
                    try:
                        sll.setColor(color)
                    except Exception:
                        pass

                # Marcadores ao longo da linha (ticks e setas)
                if isinstance(sll, QgsMarkerLineSymbolLayer):
                    sub_old = sll.subSymbol()
                    if sub_old:
                        sub = sub_old.clone()   # ✅ CLONE do sub-símbolo
                        try:
                            sub.setColor(color)
                            if hasattr(sub, "setOutlineColor"):
                                sub.setOutlineColor(color)
                        except Exception:
                            pass
                        sll.setSubSymbol(sub)

            # 3) Cria renderer novo com o símbolo clonado/editado
            new_renderer = QgsSingleSymbolRenderer(sym)
            layer.setRenderer(new_renderer)

            # (Opcional) Atualiza cor do rótulo para casar com a linha
            try:
                lbl = layer.labeling()
                if isinstance(lbl, QgsVectorLayerSimpleLabeling):
                    pal = lbl.settings()
                    fmt = pal.format()
                    fmt.setColor(color)
                    pal.setFormat(fmt)
                    layer.setLabeling(QgsVectorLayerSimpleLabeling(pal))
                    layer.setLabelsEnabled(True)
            except Exception:
                pass

            # 4) Repaint
            layer.triggerRepaint()

            # garante que o tree reflita a cor final aplicada no símbolo
            try:
                self._sync_tree_item_color(layer.id())
            except Exception:
                pass

        finally:
            # 5) Retoma renderização do canvas
            if canvas:
                canvas.setRenderFlag(True)
                try:
                    canvas.refresh()
                except Exception:
                    pass

    def _get_selected_dim_layer(self):
        """Retorna a camada (QgsVectorLayer) atualmente selecionada no treeViewCamada."""
        try:
            idxs = self.treeViewCamada.selectionModel().selectedIndexes()
        except Exception:
            return None
        if not idxs:
            return None
        idx = idxs[0]
        lyr_id = self._tv_model.data(idx, Qt.UserRole)
        lyr = QgsProject.instance().mapLayer(lyr_id)
        if isinstance(lyr, QgsVectorLayer) and lyr.geometryType() == QgsWkbTypes.LineGeometry:
            return lyr
        return None

    def _on_tv_selection_changed(self, selected, deselected):
        """Atualiza o ID da camada selecionada no treeViewCamada e sincroniza os controles da UI com essa camada."""
        idxs = self.treeViewCamada.selectionModel().selectedIndexes()
        if not idxs:
            self._tv_selected_layer_id = None
            return
        idx = idxs[0]
        self._tv_selected_layer_id = self._tv_model.data(idx, Qt.UserRole)
        # Atualiza os controles com os valores da camada selecionada
        self._sync_symbol_controls_from_layer(self._get_selected_dim_layer())

    def _criar_camada_dim_base(self, nome, crs, features, editavel=True, *, main_color=None, line_color=None, tick_color=None, arrow_color=None, label_color=None, ):
        """
        Cria uma camada de dimensionamento com estilo e rótulo padronizados,
        adiciona ao grupo 'Dimensionamento' e retorna a camada criada.

        Parâmetros de cor:
          - Você pode passar UMA cor em `main_color` (aplicada a tudo)
            ou cores específicas em `line_color`, `tick_color`, `arrow_color`, `label_color`.
          - Tipos aceitos: QColor, str ("#RRGGBB" ou nome CSS), tuple (r,g,b).
          - Se nada for passado, usa preto (compatível com chamadas antigas).
        """
        # --- helper de cor ---
        def _to_qcolor(c, fallback=QColor("black")):
            if c is None:
                return fallback
            if isinstance(c, QColor):
                return c
            if isinstance(c, str):
                qc = QColor(c)
                return qc if qc.isValid() else fallback
            if isinstance(c, (tuple, list)) and len(c) in (3, 4):
                try:
                    return QColor(*c)
                except Exception:
                    return fallback
            return fallback

        # resolve a paleta final
        base      = _to_qcolor(main_color, QColor("black"))
        col_line  = _to_qcolor(line_color,  base)
        col_tick  = _to_qcolor(tick_color,  base)
        col_arrow = _to_qcolor(arrow_color, base)
        col_label = _to_qcolor(label_color, base)

        # --- cria camada ---
        uri = f"LineString?crs={crs.authid()}"
        layer = QgsVectorLayer(uri, self._gerar_nome_unico(nome), "memory")
        prov = layer.dataProvider()
        prov.addAttributes([
            QgsField("comprimento", QVariant.Double),
            QgsField("label", QVariant.String)])
        layer.updateFields()

        # --- lê valores atuais da UI (com defaults se UI ainda não existir) ---
        try:
            arrow_size_mm = float(self.doubleSpinBoxTamanho.value())      # tamanho da seta
        except Exception:
            arrow_size_mm = 2.5
        try:
            tick_len_mm = float(self.doubleSpinBoxAux.value())            # comprimento do tick
        except Exception:
            tick_len_mm = 4.0
        try:
            text_size_pt = float(self.doubleSpinBoxTexto.value())         # tamanho do texto
        except Exception:
            text_size_pt = 8.0
        try:
            afast_mu = float(self.doubleSpinBoxRecuo.value())             # ⬅️ AGORA: afastamento (map units)
        except Exception:
            afast_mu = 0.2

        # recuo da seta (ao longo da linha) continua automático
        arrow_recuo_mm = arrow_size_mm * 0.5

        # Direção/offset perpendicular
        sinal = +1 if self.checkBoxDirecao.isChecked() else -1
        offset_mu = afast_mu * sinal

        # --- símbolo da linha ---
        line_symbol = QgsLineSymbol.createSimple({'color': 'black', 'width': '0.3'})
        base_layer = line_symbol.symbolLayer(0)
        base_layer.setOffset(offset_mu)
        base_layer.setOffsetUnit(QgsUnitTypes.RenderMapUnits)
        base_layer.setWidth(0.20)
        base_layer.setWidthUnit(QgsUnitTypes.RenderMillimeters)
        if hasattr(base_layer, "setColor"):
            base_layer.setColor(col_line)

        # --- TICK (line marker) ---
        tick_symbol = QgsMarkerSymbol.createSimple({
            'name': 'line',
            'size': str(tick_len_mm),          # usa UI
            'line_width': '0.2',
            'color': col_tick.name(),
            'angle': '0'})

        # --- SETAS (head/tail) ---
        arrow_head = QgsMarkerSymbol.createSimple({
            'name': 'triangle',
            'size': str(arrow_size_mm),        # usa UI
            'color': col_arrow.name(),
            'outline_style': 'no',
            'angle': '-90'})
        arrow_tail = QgsMarkerSymbol.createSimple({
            'name': 'triangle',
            'size': str(arrow_size_mm),        # usa UI
            'color': col_arrow.name(),
            'outline_style': 'no',
            'angle': '-90'})

        def _append_marker(placement, marker_sym, along_mm=0):
            ml = QgsMarkerLineSymbolLayer()
            ml.setPlacement(placement)
            ml.setRotateSymbols(True)
            ml.setOffset(offset_mu)
            ml.setOffsetUnit(QgsUnitTypes.RenderMapUnits)
            ml.setOffsetAlongLine(along_mm)
            ml.setOffsetAlongLineUnit(QgsUnitTypes.RenderMillimeters)
            # garante cor e tamanho no subSymbol (além do createSimple)
            sub = marker_sym.clone()
            try:
                # cor
                sub.setColor(col_tick if marker_sym == tick_symbol else col_arrow)
                if hasattr(sub, "setOutlineColor"):
                    sub.setOutlineColor(col_tick if marker_sym == tick_symbol else col_arrow)
                # tamanho (reforça o que já veio no createSimple)
                if marker_sym == tick_symbol:
                    sub.setSize(tick_len_mm)
                else:
                    sub.setSize(arrow_size_mm)
            except Exception:
                pass
            ml.setSubSymbol(sub)
            line_symbol.appendSymbolLayer(ml)

        # ticks (sem recuo)
        for plc in (QgsMarkerLineSymbolLayer.FirstVertex, QgsMarkerLineSymbolLayer.LastVertex):
            _append_marker(plc, tick_symbol, along_mm=0)

        # setas (com recuo da UI)
        _append_marker(QgsMarkerLineSymbolLayer.FirstVertex, arrow_head, arrow_recuo_mm)
        _append_marker(QgsMarkerLineSymbolLayer.LastVertex,  arrow_tail, arrow_recuo_mm)

        layer.renderer().setSymbol(line_symbol)

        # --- feições ---
        prov.addFeatures(features)

        # --- campos ocultos ---
        self.configurar_campo_oculto(layer, ["comprimento", "label"])

        # --- rotulagem (cor + tamanho do texto vindos da UI) ---
        self.configurar_etiquetas(layer, sinal_lado=(+1 if self.checkBoxDirecao.isChecked() else -1), distancia_mu=abs(offset_mu))

        # --- adiciona ao grupo "Dimensionamento" ---
        root = QgsProject.instance().layerTreeRoot()
        grupo_dim = next(
            (c for c in root.children()
             if c.name() == "Dimensionamento" and isinstance(c, QgsLayerTreeGroup)), None)
        if not grupo_dim:
            grupo_dim = root.addGroup("Dimensionamento")

        QgsProject.instance().addMapLayer(layer, False)
        grupo_dim.addLayer(layer)

        # --- bloqueia edição se necessário ---
        if not editavel:
            layer.startEditing()
            layer.commitChanges()
            # layer.setReadOnly(True)

        return layer

    def _rebuild_dim_angular(self, layer: QgsVectorLayer, use_interno: bool | None = None, radius_mu: float | None = None, min_angle_deg: float | None = None):
        """Recria os arcos de uma camada angular mantendo estilo/cores e
        ATUALIZA o mapa de links por FID (evita 'fantasmas')."""
        if not bool(layer.customProperty("dim/angular", False)):
            return

        src_id = layer.customProperty("dim/src_layer_id", "")
        src = QgsProject.instance().mapLayer(src_id)
        if not isinstance(src, QgsVectorLayer):
            self.mostrar_mensagem("Camada de origem não encontrada para esta dimensão angular.", "Aviso")
            return

        if use_interno is None:
            use_interno = bool(layer.customProperty("dim/internal", True))
        if radius_mu is None:
            radius_mu = float(layer.customProperty("dim/radius_mu", 0.5))
        if min_angle_deg is None:
            min_angle_deg = float(layer.customProperty("dim/min_angle_deg", 0.0))

        connected = bool(layer.customProperty("dim/connected", False))

        layer.setReadOnly(False)
        prov = layer.dataProvider()

        # 1) zera camada e (se conectada) o mapa de links
        prov.deleteFeatures([f.id() for f in layer.getFeatures()])
        if connected:
            self._ang_conn_links.clear()

        # 2) re-adiciona POR FEIÇÃO DE ORIGEM e atualiza o mapa de IDs
        for feat in src.getFeatures():
            if connected and feat.id() in self._ang_conn_links and self._ang_conn_links[feat.id()]:
                continue  # já tem arco gerado, não recria

            # mesma lógica de geração de arcos que você já usa
            geom = feat.geometry()
            if not geom or geom.isEmpty():
                if connected:
                    self._ang_conn_links[feat.id()] = []
                continue

            parts = geom.asMultiPolygon() if geom.isMultipart() else [geom.asPolygon()]
            to_add = []

            for part in parts:
                for ring in part:
                    n = len(ring)
                    if n < 3:
                        continue
                    m = n - 1
                    for i in range(m):
                        p_prev = QgsPointXY(ring[(i-1) % m]); p = QgsPointXY(ring[i]); p_next = QgsPointXY(ring[(i+1) % m])

                        vx1, vy1 = (p_prev.x()-p.x(), p_prev.y()-p.y())
                        vx2, vy2 = (p_next.x()-p.x(), p_next.y()-p.y())
                        l1 = math.hypot(vx1, vy1); l2 = math.hypot(vx2, vy2)
                        if l1 == 0 or l2 == 0:
                            continue
                        ux1, uy1 = vx1/l1, vy1/l1; ux2, uy2 = vx2/l2, vy2/l2

                        dot   = max(-1.0, min(1.0, ux1*ux2 + uy1*uy2))
                        ang   = math.degrees(math.acos(dot))
                        cross = ux1*uy2 - uy1*ux2
                        if cross < 0:
                            ang = 360.0 - ang

                        ang_final = ang if use_interno else (360.0 - ang)
                        if ang_final < float(min_angle_deg):
                            continue

                        a1 = math.atan2(uy1, ux1); a2 = math.atan2(uy2, ux2)
                        delta_short = ((a2 - a1 + math.pi) % (2*math.pi)) - math.pi
                        delta_long  = delta_short - 2*math.pi if delta_short >= 0 else delta_short + 2*math.pi
                        interno_pede_longo = (ang > 180.0)
                        want_long = interno_pede_longo if use_interno else (not interno_pede_longo)
                        delta = delta_long if want_long else delta_short

                        steps = max(24, int(abs(delta) / (math.pi/180.0)))
                        pts = [QgsPointXY(p.x() + radius_mu*math.cos(a1 + delta*(k/steps)),
                                          p.y() + radius_mu*math.sin(a1 + delta*(k/steps)))
                               for k in range(steps+1)]

                        f = QgsFeature()
                        f.setGeometry(QgsGeometry.fromPolylineXY(pts))
                        f.setAttributes([round(ang_final, 2), f"{ang_final:.1f}°"])
                        to_add.append(f)

            # adiciona os arcos desta feição e atualiza links
            if to_add:
                ok, new_feats = prov.addFeatures(to_add)
                if connected:
                    self._ang_conn_links[feat.id()] = [nf.id() for nf in new_feats]
            else:
                if connected:
                    self._ang_conn_links[feat.id()] = []

        layer.updateExtents()
        layer.setReadOnly(True)

        # 3) força redesenho imediato do layer e do canvas
        layer.triggerRepaint()
        try:
            self.iface.mapCanvas().refresh()
        except Exception:
            pass

        # 4) persiste propriedades
        layer.setCustomProperty("dim/internal", bool(use_interno))
        layer.setCustomProperty("dim/radius_mu", float(radius_mu))
        layer.setCustomProperty("dim/min_angle_deg", float(min_angle_deg))

    def _on_direction_changed_selected(self, *_):
        """Reage à mudança do checkBoxDirecao: alterna o lado da dimensão ou reconstrói arcos se a camada for angular."""
        layer = self._get_selected_dim_layer()
        if not layer:
            return

        # Se for angular, reconstrói os arcos (interno/suplementar)
        if bool(layer.customProperty("dim/angular", False)):
            try:
                radius_mu = float(self.doubleSpinBoxRecuo.value())
            except Exception:
                radius_mu = float(layer.customProperty("dim/radius_mu", 0.5))
            self._rebuild_dim_angular(
                layer,
                use_interno=self.checkBoxDirecao.isChecked(),
                radius_mu=radius_mu,
                min_angle_deg=float(layer.customProperty("dim/min_angle_deg", self.doubleSpinBoxTamanhoM.value())))
            return

        # caso não-angular, mantém seu comportamento atual
        try:
            arrow = float(self.doubleSpinBoxTamanho.value())
            tick  = float(self.doubleSpinBoxAux.value())
            text  = float(self.doubleSpinBoxTexto.value())
            afast = float(self.doubleSpinBoxRecuo.value())
        except Exception:
            return
        self._apply_dim_sizes(layer, arrow, tick, text, afast)

    def _sync_symbol_controls_from_layer(self, layer: QgsVectorLayer):
        """Sincroniza os valores dos controles da UI (seta, tick, recuo e texto) com o estilo atual da camada selecionada."""
        if not layer or not layer.renderer():
            return
        sym = layer.renderer().symbol()
        if not sym:
            return

        arrow_size = tick_len = afast_mu = None

        is_angular = bool(layer.customProperty("dim/angular", False))
        if is_angular:
            # para angular, o 'recuo' da UI representa o RAIO salvo na camada
            afast_mu = float(layer.customProperty("dim/radius_mu", self.doubleSpinBoxRecuo.value()))
        else:
            try:
                sll0 = sym.symbolLayer(0)
                if hasattr(sll0, "offset"):
                    afast_mu = abs(float(sll0.offset()))
            except Exception:
                pass

        for i in range(sym.symbolLayerCount()):
            sll = sym.symbolLayer(i)
            if isinstance(sll, QgsMarkerLineSymbolLayer):
                sub = sll.subSymbol()
                if not sub:
                    continue
                for sl in sub.symbolLayers():
                    if isinstance(sl, QgsSimpleMarkerSymbolLayer):
                        shp = sl.shape()
                        if shp == QgsSimpleMarkerSymbolLayer.Triangle:
                            arrow_size = sl.size()
                        elif shp == QgsSimpleMarkerSymbolLayer.Line:
                            tick_len = sl.size()

        text_size = None
        try:
            lbl = layer.labeling()
            if isinstance(lbl, QgsVectorLayerSimpleLabeling):
                pal = lbl.settings()
                text_size = pal.format().size()
        except Exception:
            pass

        def _set(spin, val):
            if val is None:
                return
            spin.blockSignals(True); spin.setValue(float(val)); spin.blockSignals(False)

        _set(self.doubleSpinBoxTamanho, arrow_size if arrow_size is not None else self.doubleSpinBoxTamanho.value())
        _set(self.doubleSpinBoxAux,     tick_len   if tick_len   is not None else self.doubleSpinBoxAux.value())
        _set(self.doubleSpinBoxRecuo,   afast_mu   if afast_mu   is not None else self.doubleSpinBoxRecuo.value())
        _set(self.doubleSpinBoxTexto,   text_size  if text_size  is not None else self.doubleSpinBoxTexto.value())

    def _on_min_length_changed_selected(self, value: float):
        """
        Recalcula APENAS a camada selecionada (se for 'Conectada'),
        e reconstrói o mapa self._dim_conn_links para que edições no polígono
        apaguem as dimensões antigas corretamente.
        """
        layer = self._get_selected_dim_layer()
        if not layer:
            return

        # não é conectada → nada a fazer
        if not bool(layer.customProperty("dim/connected", False)):
            return

        # camada de origem
        src_id = layer.customProperty("dim/src_layer_id", "")
        src_layer = QgsProject.instance().mapLayer(src_id) if src_id else None
        if not isinstance(src_layer, QgsVectorLayer):
            self.mostrar_mensagem("Camada de origem não encontrada para esta dimensão.", "Aviso")
            return

        # Regera dimensões e REFAZ o mapa de links
        try:
            layer.setReadOnly(False)
            prov = layer.dataProvider()

            # apaga tudo e zera o mapa
            prov.deleteFeatures([f.id() for f in layer.getFeatures()])
            self._dim_conn_links.clear()

            # recria e salva os novos FIDs por FID de origem
            min_len = float(value)
            for feat in src_layer.getFeatures():
                feats_dim = self._gera_dimensoes_para_feat(feat, layer.crs(), comprimento_min=min_len)
                ok, new_feats = prov.addFeatures(feats_dim)
                # guarda os ids criados para este fid de origem
                self._dim_conn_links[feat.id()] = [nf.id() for nf in new_feats]

            layer.triggerRepaint()
        finally:
            layer.setReadOnly(True)

    def _apply_dim_sizes(self, layer: QgsVectorLayer, arrow_size_mm: float, tick_len_mm: float, text_size_pt: float, afast_mu: float):
        """Aplica na camada os tamanhos definidos na UI: seta, tick, texto e afastamento (offset)."""
        if not layer or not layer.renderer():
            return

        is_angular = bool(layer.customProperty("dim/angular", False))
        canvas = getattr(self.iface, "mapCanvas", lambda: None)()
        if canvas:
            canvas.setRenderFlag(False)

        try:
            sym_old = layer.renderer().symbol()
            if sym_old is None:
                return
            sym = sym_old.clone()

            # offset perpendicular: para angular, sempre 0
            sinal = +1 if self.checkBoxDirecao.isChecked() else -1
            offset_mu = 0.0 if is_angular else (afast_mu * sinal)
            arrow_recuo_mm = max(0.0, arrow_size_mm * 0.5)

            try:
                sll0 = sym.symbolLayer(0)
                if hasattr(sll0, "setOffset"):
                    sll0.setOffset(offset_mu)
                    if hasattr(sll0, "setOffsetUnit"):
                        sll0.setOffsetUnit(QgsUnitTypes.RenderMapUnits)
            except Exception:
                pass

            for i in range(sym.symbolLayerCount()):
                sll = sym.symbolLayer(i)
                if isinstance(sll, QgsMarkerLineSymbolLayer):
                    sll.setOffset(offset_mu)
                    sll.setOffsetUnit(QgsUnitTypes.RenderMapUnits)
                    sub_old = sll.subSymbol()
                    if not sub_old:
                        continue
                    sub = sub_old.clone()

                    # detecta seta ou tick
                    is_arrow = is_tick = False
                    for sl in sub.symbolLayers():
                        if isinstance(sl, QgsSimpleMarkerSymbolLayer):
                            shp = sl.shape()
                            if shp == QgsSimpleMarkerSymbolLayer.Triangle: is_arrow = True
                            elif shp == QgsSimpleMarkerSymbolLayer.Line:  is_tick  = True

                    if is_arrow:
                        for sl in sub.symbolLayers():
                            if isinstance(sl, QgsSimpleMarkerSymbolLayer):
                                sl.setSize(arrow_size_mm)
                        sll.setOffsetAlongLine(arrow_recuo_mm)
                        sll.setOffsetAlongLineUnit(QgsUnitTypes.RenderMillimeters)

                    if is_tick:
                        for sl in sub.symbolLayers():
                            if isinstance(sl, QgsSimpleMarkerSymbolLayer):
                                sl.setSize(tick_len_mm)

                    sll.setSubSymbol(sub)

            layer.setRenderer(QgsSingleSymbolRenderer(sym))

            # Rótulo
            try:
                lbl = layer.labeling()
                if not isinstance(lbl, QgsVectorLayerSimpleLabeling):
                    self.configurar_etiquetas(layer)  # cairá no caminho (a)
                else:
                    pal = lbl.settings()
                    fmt = pal.format()
                    fmt.setSize(text_size_pt)
                    fmt.setSizeUnit(QgsUnitTypes.RenderPoints)
                    pal.setFormat(fmt)

                    # lado/distância sincronizados com o símbolo
                    off = self._get_rendered_offset_mu(layer, default_val=(afast_mu * (+1 if self.checkBoxDirecao.isChecked() else -1)))
                    pal.dist = abs(off)
                    pal.distUnits = QgsUnitTypes.RenderMapUnits
                    pal.placementFlags = (QgsPalLayerSettings.BelowLine if off >= 0 else QgsPalLayerSettings.AboveLine)

                    pal.placement = QgsPalLayerSettings.Line
                    pal.curved = True
                    layer.setLabeling(QgsVectorLayerSimpleLabeling(pal))
                    layer.setLabelsEnabled(True)
            except Exception:
                pass

            layer.triggerRepaint()
        finally:
            if canvas:
                canvas.setRenderFlag(True)
                try: canvas.refresh()
                except Exception: pass

    def _on_symbol_controls_changed(self, *_):
        """Atualiza em tempo real o estilo da dimensão quando algum controle de tamanho/recuo da UI é modificado."""
        layer = self._get_selected_dim_layer()
        if not layer:
            return

        # lê os controles da UI
        try:
            arrow_size   = float(self.doubleSpinBoxTamanho.value())
            tick_len     = float(self.doubleSpinBoxAux.value())
            text_size_pt = float(self.doubleSpinBoxTexto.value())
            raio_mu      = float(self.doubleSpinBoxRecuo.value())   # para angular = RAIO
        except Exception:
            return

        if bool(layer.customProperty("dim/angular", False)):
            self._apply_dim_sizes(layer, arrow_size, tick_len, text_size_pt, afast_mu=0.0)

            # ✅ persiste o novo raio (em METROS) para o live-edit usar
            layer.setCustomProperty("dim/radius_mu", raio_mu)

            # rebuild com o novo RAIO e direção atual
            self._rebuild_dim_angular(
                layer,
                use_interno=self.checkBoxDirecao.isChecked(),
                radius_mu=raio_mu,
                min_angle_deg=float(layer.customProperty("dim/min_angle_deg", self.doubleSpinBoxTamanhoM.value())))
            return

        # não-angular → comportamento padrão
        self._apply_dim_sizes(layer, arrow_size, tick_len, text_size_pt, afast_mu=raio_mu)

    def _gera_dim_angular_para_feat(self, feat: QgsFeature, *, use_interno: bool, radius_mu: float, min_angle_deg: float) -> list[QgsFeature]:
        """
        Gera as feições de arco para TODOS os vértices de um polígono.

        IMPORTANTE:
        - Agora `radius_mu` é interpretado como METROS (valor vindo do doubleSpinBoxRecuo).
        - Se o CRS da camada de origem for GEOGRÁFICO, convertemos metros -> graus localmente (por latitude).
        - Se o CRS for PROJETADO, usamos o fator de unidade do CRS (normalmente 1.0 para metros).
        """
        geom = feat.geometry()
        if not geom or geom.isEmpty():
            return []

        # CRS da camada de origem (para saber se é geográfico)
        src_layer = getattr(self, "_ang_conn_srcLayer", None)
        crs = src_layer.crs() if isinstance(src_layer, QgsVectorLayer) else QgsProject.instance().crs()
        is_geog = crs.isGeographic()

        # Converte "metros" -> "map units (x,y)" no ponto de referência
        # - Para CRS projetado: mesmo fator em X e Y (normalmente metros).
        # - Para CRS geográfico: fatores diferentes em X(lon) e Y(lat) (graus por metro), dependentes da latitude.
        def _radius_xy_in_mu(at_point: QgsPointXY) -> tuple[float, float]:
            if not is_geog:
                try:
                    mu_unit = crs.mapUnits()
                    # Converte METROS para a unidade do CRS (p. ex., pés se necessário)
                    factor = QgsUnitTypes.fromUnitToUnitFactor(QgsUnitTypes.DistanceMeters, mu_unit)
                except Exception:
                    factor = 1.0
                r_mu = radius_mu * factor
                return r_mu, r_mu

            # CRS geográfico → graus/metro variam com a latitude
            lat_rad = math.radians(at_point.y())
            # Fórmulas precisas (WGS84 aprox.) de metros por grau (lat/lon):
            m_per_deg_lat = 111132.92 - 559.82*math.cos(2*lat_rad) + 1.175*math.cos(4*lat_rad) - 0.0023*math.cos(6*lat_rad)
            m_per_deg_lon = 111412.84*math.cos(lat_rad) - 93.5*math.cos(3*lat_rad) + 0.118*math.cos(5*lat_rad)

            # Converte 1 metro para graus nas direções X(lon) e Y(lat)
            deg_per_m_lat = 1.0 / max(1e-9, m_per_deg_lat)
            deg_per_m_lon = 1.0 / max(1e-9, m_per_deg_lon)

            # Retorna o "raio" em unidades do mapa para X e Y separadamente
            return radius_mu * deg_per_m_lon, radius_mu * deg_per_m_lat

        feats_dim = []
        parts = geom.asMultiPolygon() if geom.isMultipart() else [geom.asPolygon()]
        for part in parts:
            for ring in part:
                n = len(ring)
                if n < 3:
                    continue
                m = n - 1  # ring fechado
                for i in range(m):
                    p_prev = QgsPointXY(ring[(i - 1) % m])
                    p      = QgsPointXY(ring[i])
                    p_next = QgsPointXY(ring[(i + 1) % m])

                    vx1, vy1 = (p_prev.x()-p.x(), p_prev.y()-p.y())
                    vx2, vy2 = (p_next.x()-p.x(), p_next.y()-p.y())
                    l1 = math.hypot(vx1, vy1); l2 = math.hypot(vx2, vy2)
                    if l1 == 0 or l2 == 0:
                        continue
                    ux1, uy1 = vx1/l1, vy1/l1
                    ux2, uy2 = vx2/l2, vy2/l2

                    # Ângulo no vértice
                    dot   = max(-1.0, min(1.0, ux1*ux2 + uy1*uy2))
                    ang   = math.degrees(math.acos(dot))
                    cross = ux1*uy2 - uy1*ux2
                    if cross < 0:  # vértice côncavo
                        ang = 360.0 - ang

                    ang_final = ang if use_interno else (360.0 - ang)
                    if ang_final < float(min_angle_deg):
                        continue

                    # Intervalo angular (curto/longo) conforme interno/suplementar
                    a1 = math.atan2(uy1, ux1); a2 = math.atan2(uy2, ux2)
                    delta_short = ((a2 - a1 + math.pi) % (2*math.pi)) - math.pi
                    delta_long  = delta_short - 2*math.pi if delta_short >= 0 else delta_short + 2*math.pi
                    interno_pede_longo = (ang > 180.0)
                    want_long = interno_pede_longo if use_interno else (not interno_pede_longo)
                    delta = delta_long if want_long else delta_short

                    steps = max(24, int(abs(delta) / (math.pi/180.0)))  # ~1° por ponto

                    # 🔑 Conversão local do raio (em METROS) para MU em X e Y
                    rx_mu, ry_mu = _radius_xy_in_mu(p)

                    # Gera o arco aplicando a escala anisotrópica (lon/lat) quando CRS é geográfico
                    pts = [QgsPointXY(
                                p.x() + rx_mu*math.cos(a1 + delta*(k/steps)),
                                p.y() + ry_mu*math.sin(a1 + delta*(k/steps))
                            ) for k in range(steps+1)]

                    f = QgsFeature()
                    f.setGeometry(QgsGeometry.fromPolylineXY(pts))
                    f.setAttributes([round(ang_final, 2), f"{ang_final:.1f}°"])
                    feats_dim.append(f)

        return feats_dim

    def _rebuild_dim_angular(self, layer: QgsVectorLayer, use_interno: bool | None = None, radius_mu: float | None = None, min_angle_deg: float | None = None):
        """Recria os arcos de uma camada angular mantendo estilo/cores e o mapa de links."""
        if not bool(layer.customProperty("dim/angular", False)):
            return

        src_id = layer.customProperty("dim/src_layer_id", "")
        src = QgsProject.instance().mapLayer(src_id)
        if not isinstance(src, QgsVectorLayer):
            self.mostrar_mensagem("Camada de origem não encontrada para esta dimensão angular.", "Aviso")
            return

        if use_interno is None:
            use_interno = bool(layer.customProperty("dim/internal", True))
        if radius_mu is None:
            radius_mu = float(layer.customProperty("dim/radius_mu", 0.5))
        if min_angle_deg is None:
            min_angle_deg = float(layer.customProperty("dim/min_angle_deg", 0.0))

        connected = bool(layer.customProperty("dim/connected", False))

        # ✅ garante que a propriedade está coerente com o rebuild atual
        layer.setCustomProperty("dim/radius_mu", radius_mu)
        layer.setCustomProperty("dim/internal", bool(use_interno))
        layer.setCustomProperty("dim/min_angle_deg", float(min_angle_deg))

        layer.setReadOnly(False)
        prov = layer.dataProvider()

        # Zera camada e (se conectada) o mapa de links
        prov.deleteFeatures([f.id() for f in layer.getFeatures()])
        if connected:
            self._ang_conn_links.clear()

        # (Re)adiciona por feição da origem usando SEMPRE o gerador unificado
        for feat in src.getFeatures():
            arcs = self._gera_dim_angular_para_feat(feat, use_interno=use_interno, radius_mu=radius_mu, min_angle_deg=min_angle_deg)
            if not arcs:
                if connected:
                    self._ang_conn_links[feat.id()] = []
                continue

            ok, new_feats = prov.addFeatures(arcs)
            if connected:
                self._ang_conn_links[feat.id()] = [f.id() for f in new_feats]

        layer.setReadOnly(True)
        layer.triggerRepaint()

    def criar_camada_dimensionamento_angular_conectada(self):
        """Cria a camada de arcos e conecta sinais da camada de origem (tempo real)."""
        layer_id = self.comboBoxCamada.currentData()
        if not layer_id:
            self.mostrar_mensagem("Selecione uma camada de polígono.", "Erro"); return
        src_layer = QgsProject.instance().mapLayer(layer_id)
        if not isinstance(src_layer, QgsVectorLayer):
            self.mostrar_mensagem("Camada inválida selecionada.", "Erro"); return

        # desconecta sinais antigos se já houver camada origem conectada
        try:
            if self._ang_conn_srcLayer:
                self._ang_conn_srcLayer.geometryChanged.disconnect(self._ang_on_geom_changed)
                self._ang_conn_srcLayer.featureAdded.disconnect(self._ang_on_feat_added)
                self._ang_conn_srcLayer.featureDeleted.disconnect(self._ang_on_feat_deleted)
        except Exception:
            pass

        # parâmetros UI
        min_angle_deg = float(self.doubleSpinBoxTamanhoM.value())
        raio_mu       = float(self.doubleSpinBoxRecuo.value())
        use_interno   = self.checkBoxDirecao.isChecked()

        # escolhe feições
        chk = self.findChild(QCheckBox, 'checkBoxSeleciona')
        feats_src = list(src_layer.getSelectedFeatures()) if (chk and chk.isChecked()) else list(src_layer.getFeatures())
        if not feats_src:
            self.mostrar_mensagem("Nenhuma feição encontrada para dimensionar.", "Erro"); return

        # cria camada vazia
        crs = src_layer.crs()
        layer = self._criar_camada_dim_base("Dim_Angular", crs, [], editavel=False)
        layer.setCustomProperty("dim/angular", True)
        layer.setCustomProperty("dim/connected", True)
        layer.setCustomProperty("dim/src_layer_id", src_layer.id())
        layer.setCustomProperty("dim/radius_mu", raio_mu)
        layer.setCustomProperty("dim/min_angle_deg", min_angle_deg)
        layer.setCustomProperty("dim/internal", bool(use_interno))

        # aplica tamanhos (para angular offset = 0)
        try:
            arrow = float(self.doubleSpinBoxTamanho.value())
            tick  = float(self.doubleSpinBoxAux.value())
            text  = float(self.doubleSpinBoxTexto.value())
        except Exception:
            arrow, tick, text = 2.5, 4.0, 8.0
        self._apply_dim_sizes(layer, arrow, tick, text, afast_mu=0.0)

        # guarda refs e popula
        self._ang_conn_layer      = layer
        self._ang_conn_layer_id   = layer.id()
        self._ang_conn_srcLayer   = src_layer
        self._ang_conn_links      = {}

        prov = layer.dataProvider()
        for feat in feats_src:
            arcs = self._gera_dim_angular_para_feat(feat, use_interno=use_interno, radius_mu=raio_mu, min_angle_deg=min_angle_deg)
            ok, new_feats = prov.addFeatures(arcs)
            self._ang_conn_links[feat.id()] = [f.id() for f in new_feats]

        layer.setReadOnly(True)
        layer.triggerRepaint()

        # conecta sinais (tempo real) - só 1 vez, depois de garantir desconexão anterior
        src_layer.geometryChanged.connect(self._ang_on_geom_changed)
        src_layer.featureAdded.connect(self._ang_on_feat_added)
        src_layer.featureDeleted.connect(self._ang_on_feat_deleted)

        self.mostrar_mensagem("Dimensão Angular Conectada criada.", "Sucesso")
        self.layer_dimensoes = layer
        self.atualizar_treeViewCamadas()
        try:
            self.iface.mapCanvas().refresh()
        except Exception:
            pass

    def _ang_layer(self):
        """Retorna a camada de dimensão angular conectada, ou None se não existir."""
        return QgsProject.instance().mapLayer(getattr(self, "_ang_conn_layer_id", ""))

    def _ang_on_geom_changed(self, fid, new_geom):
        """Atualiza os arcos de dimensão angular quando a geometria da feição de origem é modificada."""
        layer = self._ang_layer()
        src   = self._ang_conn_srcLayer
        if not layer or not src:
            return
        use_interno = bool(layer.customProperty("dim/internal", True))
        raio_mu     = float(layer.customProperty("dim/radius_mu", 0.5))
        min_deg     = float(layer.customProperty("dim/min_angle_deg", 0.0))

        layer.setReadOnly(False)
        prov = layer.dataProvider()
        prov.deleteFeatures(self._ang_conn_links.get(fid, []))

        feat = src.getFeature(fid)
        arcs = self._gera_dim_angular_para_feat(feat, use_interno=use_interno, radius_mu=raio_mu, min_angle_deg=min_deg)
        ok, new_feats = prov.addFeatures(arcs)
        self._ang_conn_links[fid] = [f.id() for f in new_feats]

        layer.setReadOnly(True)
        layer.triggerRepaint()

    def _ang_on_feat_added(self, fid):
        """Cria arcos de dimensão angular quando uma nova feição é adicionada na camada de origem."""
        layer = self._ang_layer()
        src   = self._ang_conn_srcLayer
        if not layer or not src:
            return
        use_interno = bool(layer.customProperty("dim/internal", True))
        raio_mu     = float(layer.customProperty("dim/radius_mu", 0.5))
        min_deg     = float(layer.customProperty("dim/min_angle_deg", 0.0))

        feat = src.getFeature(fid)
        arcs = self._gera_dim_angular_para_feat(feat, use_interno=use_interno, radius_mu=raio_mu, min_angle_deg=min_deg)
        layer.setReadOnly(False)
        prov = layer.dataProvider()
        ok, new_feats = prov.addFeatures(arcs)
        self._ang_conn_links[fid] = [f.id() for f in new_feats]
        layer.setReadOnly(True)
        layer.triggerRepaint()

    def _ang_on_feat_deleted(self, fid):
        """Remove os arcos de dimensão angular associados quando uma feição da camada de origem é deletada."""
        layer = self._ang_layer()
        if not layer:
            return
        layer.setReadOnly(False)
        prov = layer.dataProvider()
        prov.deleteFeatures(self._ang_conn_links.pop(fid, []))
        layer.setReadOnly(True)
        layer.triggerRepaint()

    def _mm_to_mu(self, mm: float) -> float:
        """
        Converte mm (tamanho visual) aproximando para unidades do mapa (MU) conforme o canvas atual.
        Útil p/ seta/tick caso queira respeitar mm. Se preferir, fixe um fator ou trate tudo em MU.
        """
        try:
            canvas = self.iface.mapCanvas()
            mu_per_px = canvas.mapUnitsPerPixel()
            dpi = 96.0  # pode ler do Qt, mas 96 já é ok
            return mm * (dpi / 25.4) * mu_per_px
        except Exception:
            return mm  # fallback

    def _qcolor_to_rgb(self, qcol: QColor) -> tuple[int,int,int]:
        """Converte um QColor para uma tupla RGB (r, g, b)."""
        return (qcol.red(), qcol.green(), qcol.blue())

    def _circle_from_3pts(self, p1, p2, p3):
        """Retorna (cx, cy, r) do círculo pelos 3 pontos; None se colinear."""
        (x1,y1),(x2,y2),(x3,y3) = ( (p1.x(),p1.y()), (p2.x(),p2.y()), (p3.x(),p3.y()) )
        d = 2 * (x1*(y2-y3) + x2*(y3-y1) + x3*(y1-y2))
        if abs(d) < 1e-12:
            return None
        ux = ((x1**2+y1**2)*(y2-y3) + (x2**2+y2**2)*(y3-y1) + (x3**2+y3**2)*(y1-y2)) / d
        uy = ((x1**2+y1**2)*(x3-x2) + (x2**2+y2**2)*(x1-x3) + (x3**2+y3**2)*(x2-x1)) / d
        r  = math.hypot(x1-ux, y1-uy)
        return ux, uy, r

    def _angle_deg(self, cx, cy, px, py):
        """Calcula o ângulo em graus entre o centro (cx, cy) e o ponto (px, py), no intervalo [0, 360)."""
        return (math.degrees(math.atan2(py-cy, px-cx)) + 360.0) % 360.0

    def _is_ccw_triplet(self, a_start, a_mid, a_end):
        """True se indo CCW de start→end você passa por mid (mod 360)."""
        ccw = (a_end - a_start) % 360.0
        t   = (a_mid - a_start) % 360.0
        return t <= ccw

    def _add_arrow(self, msp, tip_xy, dir_angle_deg, size_mu, color=None, layer_name=None):
        """
        Desenha uma seta (triângulo) com a PONTA em 'tip_xy'.
        'dir_angle_deg' é a direção para onde a seta aponta (para dentro da cota).
        O triângulo se estende da ponta para dentro (base a +size_mu na direção).
        """
        import ezdxf
        if size_mu <= 0:
            return

        ang = math.radians(dir_angle_deg % 360.0)
        ux, uy = math.cos(ang), math.sin(ang)   # direção "para dentro"
        bx, by = -uy, ux                        # perpendicular (base)

        # base no interior, a 'size_mu' da ponta
        base_cx = tip_xy[0] + ux * size_mu
        base_cy = tip_xy[1] + uy * size_mu

        w = 0.6 * size_mu  # largura da base
        p1 = (base_cx + bx * (w * 0.5), base_cy + by * (w * 0.5))
        p2 = (base_cx - bx * (w * 0.5), base_cy - by * (w * 0.5))

        attrs = {'layer': layer_name}
        if color:
            try:
                attrs['true_color'] = ezdxf.colors.rgb2int(color)
            except Exception:
                pass

        # SOLID: 3 pontos (o 4º repete o 3º)
        msp.add_solid([tip_xy, p1, p2, p2], dxfattribs=attrs)

    def _normalize_text_rotation_deg(self, rot_deg: float, *, eps: float = 0.5) -> float:
        """
        Mantém o texto 'em pé' (upright). Se a rotação ficar entre 90° e 270°,
        ajusta em -180°. Retorna no intervalo [0, 360).
        eps: pequena folga para evitar flip ao redor de 90°/270°.
        """
        a = float(rot_deg) % 360.0
        if (90.0 + eps) < a < (270.0 - eps):
            a -= 180.0
        if a < 0.0:
            a += 360.0
        return a

    def _add_mtext_centered(self, msp, text, at_xy, height_mu, rotation_deg, color=None, layer_name=None):
        """
        MTEXT centralizado com rotação normalizada (não vira de cabeça para baixo).
        """
        import ezdxf

        # normaliza a rotação para manter texto 'em pé'
        rotation_deg = self._normalize_text_rotation_deg(rotation_deg)

        # alinhamento (com fallbacks por versão)
        try:
            from ezdxf.enums import MTextEntityAlignment as MEA
            align_value = MEA.MIDDLE_CENTER
        except Exception:
            try:
                from ezdxf.lldxf.const import MTEXT_MIDDLE_CENTER
                align_value = MTEXT_MIDDLE_CENTER
            except Exception:
                align_value = 5  # MIDDLE_CENTER habitual

        attrs = {
            'layer': layer_name,
            'char_height': float(height_mu),
            'attachment_point': align_value}
        if color:
            try:
                attrs['true_color'] = ezdxf.colors.rgb2int(color)
            except Exception:
                pass

        mt = msp.add_mtext(text, dxfattribs=attrs)
        # posicionamento + rotação (assina pode ou não aceitar 'rotation')
        try:
            mt.set_location(at_xy, rotation=float(rotation_deg))
        except TypeError:
            try:
                mt.set_location(at_xy)
            except Exception:
                try:
                    mt.dxf.insert = at_xy
                except Exception:
                    pass
            try:
                mt.dxf.rotation = float(rotation_deg)
            except Exception:
                pass

        return mt

    def _add_tick_segment(self, msp, at_xy, base_dir_deg, length_mu, *, layer_name=None, true_color=None, perpendicular=True):
        """
        Desenha um 'tick' (linha auxiliar) centrado em at_xy.
        - base_dir_deg: ângulo base em graus (da linha ou da tangente do arco).
        - perpendicular=True -> o tick é desenhado perpendicular ao ângulo base.
        """
        ang = (base_dir_deg + (90.0 if perpendicular else 0.0)) % 360.0
        th  = math.radians(ang)
        dx  = math.cos(th) * (length_mu * 0.5)
        dy  = math.sin(th) * (length_mu * 0.5)

        pA = (at_xy[0] - dx, at_xy[1] - dy)
        pB = (at_xy[0] + dx, at_xy[1] + dy)

        attrs = {'layer': layer_name}
        if true_color is not None:
            attrs['true_color'] = true_color

        msp.add_line(pA, pB, dxfattribs=attrs)

    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,
            "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 exportar_dim_layer_para_dxf(self):
        """Exporta a camada de dimensionamento selecionada para um arquivo DXF, incluindo linhas, setas, ticks e rótulos."""
        # 0) Pega a camada selecionada no treeViewCamada
        layer = self._get_selected_dim_layer()
        if not isinstance(layer, QgsVectorLayer) or layer.geometryType() != QgsWkbTypes.LineGeometry:
            self.mostrar_mensagem("Selecione uma camada de dimensionamento (linhas).", "Erro")
            return

        # 1) Escolher local/arquivo usando o gerenciador de salvamento
        #    (gera nome único no último diretório usado e garante extensão .dxf)
        nome_sugestao = f"{layer.name()}_Dim.dxf" if layer.name() else "Dimensionamento.dxf"
        path = self.escolher_local_para_salvar(nome_sugestao, "Arquivos DXF (*.dxf)")
        if not path:
            # usuário cancelou
            return

        doc = ezdxf.new(dxfversion="R2010")
        # define unidades (assumindo metros se o CRS for projetado em metros)
        try:
            if QgsProject.instance().crs().mapUnits() == QgsUnitTypes.DistanceMeters:
                doc.units = ezdxf.units.M
            else:
                doc.units = ezdxf.units.Unitless
        except Exception:
            doc.units = ezdxf.units.Unitless

        # 3) adiciona layer DXF
        dxf_layer_name = layer.name()
        # pega cor predominante do símbolo
        color_rgb = (0,0,0)
        try:
            sym = layer.renderer().symbol()
            if sym and sym.symbolLayerCount() > 0:
                sll0 = sym.symbolLayer(0)
                if hasattr(sll0, "color"):
                    qcol = sll0.color()
                    color_rgb = self._qcolor_to_rgb(qcol)
        except Exception:
            pass

        if dxf_layer_name not in doc.layers:
            doc.layers.add(dxf_layer_name, true_color=ezdxf.colors.rgb2int(color_rgb))
        msp = doc.modelspace()

        rgb_int = ezdxf.colors.rgb2int(color_rgb)  # converte a tupla RGB para int do DXF

        # 4) tamanhos (conversão simples)
        # Seta em UNIDADES DO DESENHO (usa o valor do spin diretamente)
        try:
            ui_arrow = float(self.doubleSpinBoxTamanho.value())
        except Exception:
            ui_arrow = 2.5  # fallback

        try:
            ui_tick = float(self.doubleSpinBoxAux.value())
        except Exception:
            ui_tick = 4.0  # fallback

        try:
            text_pt = float(self.doubleSpinBoxTexto.value())
        except Exception:
            text_pt = 8.0  # fallback

        # seta = 50% do valor do spin
        arrow_mu = 0.20 * ui_arrow

        # TICK = 50% do valor do doubleSpinBoxAux (em unidades do desenho)
        tick_mu = 0.20 * ui_tick

        # Altura do texto em unidades do modelo (independente do zoom)
        TEXT_SCALE = 0.03  # 3,00%  (8 -> 0.5)
        text_mu = text_pt * TEXT_SCALE

        # Recuo da UI (em MU) e lado (interno/suplementar)
        recuo_mu = float(getattr(self, "doubleSpinBoxRecuo", None).value()) if hasattr(self, "doubleSpinBoxRecuo") else 0.0
        sinal = -1 if (hasattr(self, "checkBoxDirecao") and self.checkBoxDirecao.isChecked()) else +1

        is_angular = bool(layer.customProperty("dim/angular", False))

        # 5) exporta feições
        for f in layer.getFeatures():
            g = f.geometry()
            if not g or g.isEmpty():
                continue

            if not g.isMultipart():
                pts = g.asPolyline()
            else:
                parts = g.asMultiPolyline()
                pts = parts[0] if parts else []

            if len(pts) < 2:
                continue

            if is_angular and len(pts) >= 3:
                # —— ANGULAR: reconstrói ARC verdadeiro a partir de 3 pontos (início, meio, fim)
                p1 = pts[0]; pm = pts[len(pts)//2]; p2 = pts[-1]
                circ = self._circle_from_3pts(p1, pm, p2)
                if circ:
                    cx, cy, r = circ
                    a1 = self._angle_deg(cx, cy, p1.x(), p1.y())
                    am = self._angle_deg(cx, cy, pm.x(), pm.y())   # ângulo do ponto médio
                    a2 = self._angle_deg(cx, cy, p2.x(), p2.y())

                    # sentido do arco: CCW se indo de a1→a2 passa por 'am'
                    ccw = self._is_ccw_triplet(a1, am, a2)
                    start_deg, end_deg = (a1, a2) if ccw else (a2, a1)

                    # >>> NOVO: raio exportado vem do doubleSpinBoxRecuo (se > 0)
                    R = recuo_mu if recuo_mu > 0 else r

                    # arco DXF com o raio escolhido
                    msp.add_arc(center=(cx, cy), radius=R, start_angle=start_deg, end_angle=end_deg,
                                dxfattribs={'layer': dxf_layer_name, 'true_color': rgb_int})

                    # --- Direções das setas/ticks pela PRÓPRIA polilinha (mesma lógica que você aprovou)
                    tan_start = math.degrees(math.atan2(pts[1].y() - pts[0].y(),   pts[1].x() - pts[0].x()))
                    tan_end   = math.degrees(math.atan2(pts[-2].y() - pts[-1].y(), pts[-2].x() - pts[-1].x()))

                    # Endpoints do arco com o raio R
                    p1_xy = (cx + R * math.cos(math.radians(start_deg)),
                             cy + R * math.sin(math.radians(start_deg)))
                    p2_xy = (cx + R * math.cos(math.radians(end_deg)),
                             cy + R * math.sin(math.radians(end_deg)))

                    # Tangentes corretas no endpoint do NOVO arco (apontando para "dentro" do arco)
                    if ccw:
                        tan_start = (start_deg + 90.0) % 360.0
                        tan_end   = (end_deg   - 90.0) % 360.0
                    else:
                        tan_start = (start_deg - 90.0) % 360.0
                        tan_end   = (end_deg   + 90.0) % 360.0

                    # Setas: a função _add_arrow já usa 'dir_angle_deg' como direção "para dentro"
                    self._add_arrow(msp, p1_xy, tan_start, arrow_mu, color=color_rgb, layer_name=dxf_layer_name)
                    self._add_arrow(msp, p2_xy, tan_end,   arrow_mu, color=color_rgb, layer_name=dxf_layer_name)

                    # Ticks: perpendiculares à tangente local
                    if tick_mu > 0:
                        self._add_tick_segment(msp, p1_xy, base_dir_deg=tan_start, length_mu=tick_mu,
                                               layer_name=dxf_layer_name, true_color=rgb_int, perpendicular=True)
                        self._add_tick_segment(msp, p2_xy, base_dir_deg=tan_end,   length_mu=tick_mu,
                                               layer_name=dxf_layer_name, true_color=rgb_int, perpendicular=True)

                    # —— MTEXT SOBRE A LINHA: mesmo ângulo 'am' do ponto médio, mas no raio R
                    # rotação continua pela tangente pb→pa (como no seu trecho)
                    mid_idx = len(pts) // 2
                    if 0 < mid_idx < len(pts) - 1:
                        pb, pa = pts[mid_idx - 1], pts[mid_idx + 1]
                        trot = math.degrees(math.atan2(pa.y() - pb.y(), pa.x() - pb.x()))
                    else:
                        # fallback por segurança
                        if ccw:
                            sweep = (end_deg - start_deg) % 360.0
                            amid  = (start_deg + sweep * 0.5) % 360.0
                            trot  = (amid + 90.0) % 360.0
                        else:
                            sweep = (start_deg - end_deg) % 360.0
                            amid  = (start_deg - sweep * 0.5) % 360.0
                            trot  = (amid - 90.0) % 360.0

                    # >>> posição do texto exatamente sobre o arco de raio R
                    tx = cx + R * math.cos(math.radians(am))
                    ty = cy + R * math.sin(math.radians(am))
                    txt = f.attribute("label") or f"{f.attribute(0):.1f}°"
                    self._add_mtext_centered(msp, txt, (tx, ty), text_mu, trot,
                                             color=color_rgb, layer_name=dxf_layer_name)
                else:
                    # fallback: exporta a polilinha como está
                    msp.add_lwpolyline([(p.x(), p.y()) for p in pts],
                                       dxfattribs={'layer': dxf_layer_name, 'true_color': rgb_int})

            else:
                # —— LINEAR: polilinha com OFFSET (= recuo_mu * sinal), setas, ticks e texto
                p1, p2 = pts[0], pts[-1]
                ang = math.degrees(math.atan2(p2.y()-p1.y(), p2.x()-p1.x()))
                ang_rad = math.radians(ang)

                # vetor normal (perpendicular) para aplicar o afastamento
                nx = -math.sin(ang_rad) * (recuo_mu * sinal)
                ny =  math.cos(ang_rad) * (recuo_mu * sinal)

                # aplica o offset em TODOS os vértices (suporta multi-segmento)
                pts_off = [(p.x() + nx, p.y() + ny) for p in pts]

                # polilinha deslocada
                msp.add_lwpolyline(pts_off, dxfattribs={'layer': dxf_layer_name, 'true_color': rgb_int})

                # setas: ponta nas extremidades deslocadas; direção para "dentro" da cota
                p1_off = pts_off[0]; p2_off = pts_off[-1]
                self._add_arrow(msp, p1_off, ang,      arrow_mu, color=color_rgb, layer_name=dxf_layer_name)
                self._add_arrow(msp, p2_off, ang+180,  arrow_mu, color=color_rgb, layer_name=dxf_layer_name)

                # ticks perpendiculares à linha
                if tick_mu > 0:
                    self._add_tick_segment(msp, p1_off, base_dir_deg=ang, length_mu=tick_mu,
                                           layer_name=dxf_layer_name, true_color=rgb_int, perpendicular=True)
                    self._add_tick_segment(msp, p2_off, base_dir_deg=ang, length_mu=tick_mu,
                                           layer_name=dxf_layer_name, true_color=rgb_int, perpendicular=True)

                # texto no centro da linha deslocada
                midx = 0.5*(p1_off[0]+p2_off[0]); midy = 0.5*(p1_off[1]+p2_off[1])
                label = f.attribute("label")
                if label:
                    self._add_mtext_centered(msp, label, (midx, midy), text_mu, ang, color=color_rgb, layer_name=dxf_layer_name)

        # 6) salva
        try:
            doc.saveas(path)
        except Exception as e:
            self.mostrar_mensagem(f"Falha ao salvar DXF: {e}", "Erro")
            return

        self.mostrar_mensagem(f"DXF exportado com sucesso em:\n{os.path.basename(path)}", "Sucesso", caminho_pasta=os.path.dirname(path), caminho_arquivo=path)

class TreeDeleteButtonDelegate(QStyledItemDelegate):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.parent = parent  # diálogo (tem self.iface)

    def paint(self, painter, option, index):
        """Desenha cada item do treeViewCamada com fundo customizado, texto em negrito e botão 'X' para remover a camada."""
        # pinta normal para linhas inválidas
        if not index.isValid():
            return super().paint(painter, option, index)

        # linha “header” não é um item: no nosso caso usamos header do QTreeView,
        # então todos os índices são itens reais. Vamos customizar tudo.

        # Fundo
        if option.state & QStyle.State_Selected:
            painter.fillRect(option.rect, option.palette.highlight())
        elif option.state & QStyle.State_MouseOver:
            painter.fillRect(option.rect, QColor("#E8F4FF"))
        else:
            painter.fillRect(option.rect, option.palette.base())

        # Layout (desenha “X” + texto)
        rect = option.rect
        icon_size = 11
        icon_margin = 6
        icon_rect = QRect(rect.left() + icon_margin,
                          rect.top() + (rect.height() - icon_size) // 2,
                          icon_size, icon_size)
        text_rect = QRect(icon_rect.right() + icon_margin,
                          rect.top(),
                          rect.width() - icon_size - 3*icon_margin,
                          rect.height())

        # Botão X
        painter.save()
        painter.setRenderHint(QPainter.Antialiasing)
        painter.setPen(QPen(QColor(0, 0, 255), 1))
        painter.setBrush(QBrush(QColor(255, 0, 0, 200)))
        painter.drawRoundedRect(icon_rect, 2, 2)
        painter.setPen(QPen(QColor(255, 255, 255), 2))
        painter.drawLine(icon_rect.topLeft() + QPoint(3, 3), icon_rect.bottomRight() - QPoint(3, 3))
        painter.drawLine(icon_rect.topRight() + QPoint(-3, 3), icon_rect.bottomLeft() + QPoint(3, -3))
        painter.restore()

        # Texto (em negrito) — respeita Qt.ForegroundRole do item
        painter.save()
        text = index.data(Qt.DisplayRole)

        # tenta pegar a cor do item (setForeground)
        brush = index.data(Qt.ForegroundRole)
        if isinstance(brush, QBrush) and brush.color().isValid():
            text_color = brush.color()
        else:
            # fallback para a paleta (selecionado x normal)
            text_color = (option.palette.highlightedText().color()
                          if option.state & QStyle.State_Selected
                          else option.palette.text().color())

        painter.setPen(QPen(text_color))
        font = painter.font()
        font.setBold(True)
        painter.setFont(font)
        painter.drawText(text_rect, Qt.AlignVCenter | Qt.TextSingleLine, text)
        painter.restore()

    @staticmethod
    def remove_layer_and_cleanup(layer_id, group_name="Dimensionamento"):
        """Remove a camada pelo ID e apaga o grupo 'Dimensionamento' se ele ficar vazio."""
        from qgis.core import QgsProject
        project = QgsProject.instance()
        layer = project.mapLayer(layer_id)
        if layer:
            project.removeMapLayer(layer_id)
            root = project.layerTreeRoot()
            group = root.findGroup(group_name)
            if group and len(group.children()) == 0:
                parent = group.parent()
                if parent:
                    parent.removeChildNode(group)

    def editorEvent(self, event, model, option, index):
        """Detecta clique no botão 'X' da lista e remove a camada correspondente do projeto."""
        if event.type() == QEvent.MouseButtonRelease:
            rect = option.rect
            icon_size = 12
            icon_margin = 6
            icon_rect = QRect(rect.left() + icon_margin,
                              rect.top() + (rect.height() - icon_size) // 2,
                              icon_size, icon_size)
            if icon_rect.contains(event.pos()):
                layer_id = index.data(Qt.UserRole)
                TreeDeleteButtonDelegate.remove_layer_and_cleanup(layer_id)
                try:
                    self.parent.iface.mapCanvas().refresh()
                except Exception:
                    pass
                return True
        return super().editorEvent(event, model, option, index)
