import math import FreeCAD import Part from Utils.PVPlantUtils import findObjects if FreeCAD.GuiUp: import FreeCADGui from PySide import QtCore, QtGui, QtWidgets from PySide.QtCore import QT_TRANSLATE_NOOP import os else: # \cond def translate(ctxt, txt): return txt def QT_TRANSLATE_NOOP(ctxt, txt): return txt # \endcond __title__ = "PVPlant Export to DXF" __author__ = "Javier Braña" __url__ = "http://www.sogos-solar.com" import PVPlantResources from PVPlantResources import DirIcons as DirIcons field = {"name": "", "width": 0, "heigth": 0} def getWire(wire, nospline=False, width=.0): """Return a list of DXF ready points and bulges from a wire. It builds a list of points from the edges of a `wire`. If the edges are circular arcs, the "bulge" of that edge is calculated, for other cases, the bulge is considered zero. Parameters ---------- wire : Part::TopoShape ('Wire') A shape representing a wire. nospline : bool, optional It defaults to `False`. If it is `True`, the edges of the wire are not considered as being one of `'BSplineCurve'`, `'BezierCurve'`, or `'Ellipse'`, and a simple point is added to the list. Otherwise, `getSplineSegs(edge)` is used to extract the points and add them to the list. Returns ------- list of tuples It returns a list of tuples ``[(...), (...), ...]`` where each tuple indicates a point with additional information besides the coordinates. Two types of tuples may be returned. [(float, float, float, None, None, float), ...] When `lw` is `True` (`'lwpolyline'`) the first three values represent the coordinates of the point, the next two are `None`, and the last value is the bulge. [((float, float, float), None, [None, None], float), ...] When `lw` is `False` (`'polyline'`) the first element is a tuple of three values that indicate the coordinates of the point, the next element is `None`, the next element is a list of two `None` values, and the last element is the value of the bulge. See also -------- calcBulge """ import DraftGeomUtils import math def fmt(vec, b=0.0): return (vec.x * 0.001, vec.y * 0.001, width, width, b) points = [] edges = Part.__sortEdges__(wire.Edges) for edge in edges: v1 = edge.Vertexes[0].Point if DraftGeomUtils.geomType(edge) == "Circle": # polyline bulge -> negative makes the arc go clockwise angle = edge.LastParameter - edge.FirstParameter bul = math.tan(angle / 4) if edge.Curve.Axis.dot(FreeCAD.Vector(0, 0, 1)) < 0: bul = -bul points.append(fmt(v1, bul)) elif (DraftGeomUtils.geomType(edge) in ["BSplineCurve", "BezierCurve", "Ellipse"]) and (not nospline): spline = getSplineSegs(edge) spline.pop() for p in spline: points.append(fmt(p)) else: points.append(fmt(v1)) v = edges[-1].Vertexes[-1].Point points.append(fmt(v)) return points def getArcData(edge): """Return center, radius, start, and end angles of a circle-based edge. Parameters ---------- edge : Part::TopoShape ('Edge') An edge representing a circular arc, either open or closed. Returns ------- (tuple, float, float, float) It returns a tuple of four values; the first value is a tuple with the coordinates of the center `(x, y, z)`; the other three represent the magnitude of the radius, and the start and end angles in degrees that define the arc. (tuple, float, 0, 0) If the number of vertices in the `edge` is only one, only the center point exists, so it's a full circumference; in this case, both angles are zero. """ ce = edge.Curve.Center radius = edge.Curve.Radius if len(edge.Vertexes) == 1: # closed circle return DraftVecUtils.tup(ce), radius, 0, 0 else: # new method: recalculate ourselves as we cannot trust edge.Curve.Axis # or XAxis p1 = edge.Vertexes[0].Point p2 = edge.Vertexes[-1].Point v1 = p1.sub(ce) v2 = p2.sub(ce) # print(v1.cross(v2)) # print(edge.Curve.Axis) # print(p1) # print(p2) # we can use Z check since arcs getting here will ALWAYS be in XY plane # Z can be 0 if the arc is 180 deg # if (v1.cross(v2).z >= 0) or (edge.Curve.Axis.z > 0): # Calculates the angles of the first and last points # in the circular arc, with respect to the global X axis. if edge.Curve.Axis.z > 0: # clockwise ang1 = -DraftVecUtils.angle(v1) ang2 = -DraftVecUtils.angle(v2) else: # counterclockwise ang2 = -DraftVecUtils.angle(v1) ang1 = -DraftVecUtils.angle(v2) # obsolete method - fails a lot # if round(edge.Curve.Axis.dot(Vector(0, 0, 1))) == 1: # ang1, ang2 = edge.ParameterRange # else: # ang2, ang1 = edge.ParameterRange # if edge.Curve.XAxis != Vector(1, 0, 0): # ang1 -= DraftVecUtils.angle(edge.Curve.XAxis) # ang2 -= DraftVecUtils.angle(edge.Curve.XAxis) return (DraftVecUtils.tup(ce), radius, math.degrees(ang1), math.degrees(ang2)) # ================================================================================= # CLASE PARA EXPORTACIÓN DXF # ================================================================================= class exportDXF: def __init__(self, filename): self.doc = None self.msp = None self.filename = filename self.paper_layouts = [] ''' doc.linetypes.add("GRENZE2", # linetype definition in acad.lin: # A,.25,-.1,[BOX,ltypeshp.shx,x=-.1,s=.1],-.1,1 # replacing BOX by shape index 132 (got index from an AutoCAD file), # ezdxf can't get shape index from ltypeshp.shx pattern="A,.25,-.1,[132,ltypeshp.shx,x=-.1,s=.1],-.1,1", description="Grenze eckig ----[]-----[]----[]-----[]----[]--", length=1.45, # required for complex line types }) doc.linetypes.add("GRENZE2", # linetype definition in acad.lin: # A,.25,-.1,[BOX,ltypeshp.shx,x=-.1,s=.1],-.1,1 # replacing BOX by shape index 132 (got index from an AutoCAD file), # ezdxf can't get shape index from ltypeshp.shx pattern = "A,.25,-.1,[132,ltypeshp.shx,x=-.1,s=.1],-.1,1", description = "Límite1 ----0-----0----0-----0----0-----0--", length = 1.45, # required for complex line types }) ''' def createFile(self, version='R2018'): import ezdxf from ezdxf.tools.standards import linetypes # 1. Create a new document self.doc = ezdxf.new(version) # 2. Setup document: self.doc.header["$INSUNITS"] = 6 self.doc.header['$MEASUREMENT'] = 1 self.doc.header['$LUNITS'] = 2 self.doc.header['$AUNITS'] = 0 # 3. Add new entities to the modelspace: self.msp = self.doc.modelspace() for name, desc, pattern in linetypes(): if name not in self.doc.linetypes: self.doc.linetypes.add(name=name, pattern=pattern, description=desc, ) self.doc.linetypes.add(name="FENCELINE1", pattern="A,.25,-.1,[132,ltypeshp.shx,x=-.1,s=.1],-.1,1", description="Límite1 ----0-----0----0-----0----0-----0--", length=1.45) # required for complex line types def save(self): from os.path import exists file_exists = exists(self.filename) self.doc.saveas(self.filename) self.doc.save() def createLayer(self, layerName="newLayer", layerColor=(0,0,0), layerLineType='CONTINUOUS'): layer = self.doc.layers.add(name=layerName, linetype=layerLineType) layer.rgb = layerColor return layer def createBlock(self, blockName='newBlock'): # Create a block block = self.doc.blocks.new(name=blockName) return block def insertBlock(self, name, point=(0.0, 0.0), rotation=0.0, xscale=0.0, yscale=0.0): if name == "": return None return self.msp.add_blockref(name, point, dxfattribs={ 'xscale': xscale, 'yscale': yscale, 'rotation': rotation }) def createPolyline(self, wire, layer=""): try: data = getWire(wire.Shape) lwp = self.msp.add_lwpolyline(data) if layer: lwp.dxf.layer = layer return lwp except Exception as e: print("Error creating polyline:", e) return None def createHatch(self, wire, pattern="SOLID", scale=1.0, angle=0, layer=None): """Crea un sombreado (hatch) para un área""" try: # Obtener los puntos en metros points = [(x, y) for (x, y, *_) in wire] # Crear el hatch hatch = self.msp.add_hatch(color=7, dxfattribs={'layer': layer}) if pattern == "SOLID": # Sombreado sólido hatch.set_solid_fill() else: # Patrón de sombreado hatch.set_pattern_fill(name=pattern, scale=scale, angle=angle) # Añadir el contorno hatch.paths.add_polyline_path(points, is_closed=True) return hatch except Exception as e: print("Error creating hatch:", e) return None def export_feature_image(self, feature, layer_name): """Exporta una imagen del GeoFeature y la añade al DXF""" try: # Añadir la imagen al DXF image_def = self.doc.add_image_def(feature.ImageFile, size_in_pixel=(feature.XSize, feature.YSize)) self.msp.add_image(image_def, insert=(0, 0, 0), size_in_units=(feature.XSize, feature.YSize), rotation=0, dxfattribs={'layer': layer_name}) print(f"Imagen exportada para {feature.Label}") # Eliminar el archivo temporal import os os.unlink(temp_img.name) except Exception as e: print(f"Error en exportación de imagen: {e}") # ================================================================================= # INTERFAZ DE USUARIO # ================================================================================= class LineTypeEditorDelegate(QtWidgets.QStyledItemDelegate): """Delegado que muestra el combobox personalizado solo durante la edición""" def __init__(self, parent=None): super().__init__(parent) self.line_styles = { "CONTINUOUS": QtCore.Qt.SolidLine, "DASHED": QtCore.Qt.DashLine, "DOTTED": QtCore.Qt.DotLine, "DASH-DOT": QtCore.Qt.DashDotLine, "DASH-DOT-DOT": QtCore.Qt.DashDotDotLine, "FENCELINE1": QtCore.Qt.CustomDashLine, } self.custom_patterns = { "FENCELINE1": [4, 2, 1, 2] } # Tamaño mínimo para mostrar la previsualización self.min_preview_width = 80 def createEditor(self, parent, option, index): # SOLO se crea el editor cuando se inicia la edición return LineTypeComboBox(parent) def setEditorData(self, editor, index): # Establecer el valor actual en el editor value = index.data(QtCore.Qt.DisplayRole) if value: editor.setCurrentText(value) def setModelData(self, editor, model, index): # Guardar el valor seleccionado en el modelo model.setData(index, editor.currentText(), QtCore.Qt.EditRole) def paint0(self, painter, option, index): """Pintado normal de la celda (sin editor)""" # Configurar el fondo según selección if option.state & QtWidgets.QStyle.State_Selected: painter.fillRect(option.rect, option.palette.highlight()) else: painter.fillRect(option.rect, option.palette.base()) # Dibujar el texto del tipo de línea text = index.data(QtCore.Qt.DisplayRole) or "" painter.setPen(option.palette.text().color()) painter.drawText(option.rect.adjusted(5, 0, -5, 0), QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, text) def paint1(self, painter, option, index): # Fondo para selección if option.state & QtWidgets.QStyle.State_Selected: painter.fillRect(option.rect, option.palette.highlight()) else: painter.fillRect(option.rect, option.palette.base()) # Obtener el tipo de línea line_type = index.data(QtCore.Qt.DisplayRole) or "CONTINUOUS" line_type = line_type.upper() # Área de texto text_rect = option.rect.adjusted(5, 0, -100, 0) painter.setPen(option.palette.color(QtGui.QPalette.Text)) painter.drawText(text_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, line_type) # Área de previsualización preview_rect = option.rect.adjusted(option.rect.width() - 90, 2, -5, -2) # Configurar el estilo de línea pen = QtGui.QPen(QtGui.QColor(0, 0, 0), 2) if line_type in self.line_styles: pen.setStyle(self.line_styles[line_type]) if line_type in self.custom_patterns: pen.setDashPattern(self.custom_patterns[line_type]) else: pen.setStyle(QtCore.Qt.SolidLine) painter.setPen(pen) # Dibujar la línea center_y = preview_rect.center().y() painter.drawLine(preview_rect.left(), center_y, preview_rect.right(), center_y) def paint(self, painter, option, index): # Fondo para selección if option.state & QtWidgets.QStyle.State_Selected: painter.fillRect(option.rect, option.palette.highlight()) else: painter.fillRect(option.rect, option.palette.base()) # Obtener el tipo de línea line_type = index.data(QtCore.Qt.DisplayRole) or "CONTINUOUS" line_type = line_type.upper() # Calcular el espacio disponible total_width = option.rect.width() # Área de texto (ajustable) text_rect = option.rect.adjusted(5, 0, -5, 0) # Área de previsualización (derecha, tamaño fijo pero adaptable) preview_width = min(self.min_preview_width, total_width - 50) # 50px para texto mínimo if preview_width < 20: # No mostrar previsualización si es demasiado pequeña preview_width = 0 # Ajustar rectángulos if preview_width > 0: text_rect.setRight(option.rect.right() - preview_width - 10) preview_rect = option.rect.adjusted( option.rect.width() - preview_width, 2, -5, -2 ) else: # Solo mostrar texto si no hay espacio para previsualización preview_rect = None # Dibujar texto del tipo de línea painter.setPen(option.palette.color(QtGui.QPalette.Text)) painter.drawText(text_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, line_type) # Dibujar previsualización si hay espacio suficiente if preview_rect and preview_width > 20: # Configurar el estilo de línea pen = QtGui.QPen(QtGui.QColor(0, 0, 0), 2) if line_type in self.line_styles: pen.setStyle(self.line_styles[line_type]) if line_type in self.custom_patterns: pen.setDashPattern(self.custom_patterns[line_type]) else: pen.setStyle(QtCore.Qt.SolidLine) painter.setPen(pen) # Dibujar la línea centrada verticalmente center_y = preview_rect.center().y() painter.drawLine(preview_rect.left(), center_y, preview_rect.right(), center_y) def sizeHint(self, option, index): return QtCore.QSize(200, 25) class LineTypeDelegate(QtWidgets.QStyledItemDelegate): def __init__(self, parent=None): super().__init__(parent) self.line_types = [ {"name": "Continuous", "pen": QtCore.Qt.SolidLine}, {"name": "Dashed", "pen": QtCore.Qt.DashLine}, {"name": "Dotted", "pen": QtCore.Qt.DotLine}, {"name": "Dash-Dot", "pen": QtCore.Qt.DashDotLine}, {"name": "Dash-Dot-Dot", "pen": QtCore.Qt.DashDotDotLine} ] def paint(self, painter, option, index): # Dibujar el fondo painter.save() painter.setRenderHint(QtGui.QPainter.Antialiasing) # Configuraciones comunes line_color = QtGui.QColor(0, 0, 0) # Color de la línea line_width = 2 # Grosor de línea # Dibujar el ítem base super().paint(painter, option, index) # Obtener el tipo de línea line_type = self.line_types[index.row()] # Configurar el área de dibujo text_rect = option.rect.adjusted(5, 0, -100, 0) line_rect = option.rect.adjusted(option.rect.width() - 90, 2, -5, -2) # Dibujar el texto painter.drawText(text_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, line_type["name"]) # Dibujar la línea de muestra pen = QtGui.QPen(line_color, line_width, line_type["pen"]) painter.setPen(pen) start_y = line_rect.center().y() painter.drawLine(line_rect.left(), start_y, line_rect.right(), start_y) painter.restore() def sizeHint(self, option, index): size = super().sizeHint(option, index) size.setHeight(25) # Altura aumentada para mejor visualización return size class LineTypePreviewDelegate(QtWidgets.QStyledItemDelegate): def __init__(self, parent=None): super().__init__(parent) self.line_types = [ {"name": "Continuous", "style": QtCore.Qt.SolidLine}, {"name": "Dashed", "style": QtCore.Qt.DashLine}, {"name": "Dotted", "style": QtCore.Qt.DotLine}, {"name": "Custom 1 (--0--0--)", "style": QtCore.Qt.CustomDashLine, "pattern": [8, 4, 2, 4]}, # Patrón personalizado {"name": "Custom 2 (- - -)", "style": QtCore.Qt.CustomDashLine, "pattern": [6, 3]} # Otro patrón ] def paint(self, painter, option, index): # SOLUCIÓN AL PROBLEMA: Usar un enfoque más simple sin painter.save/restore try: if index.row() >= len(self.line_types): return # Configurar el fondo para selección if option.state & QtGui.QStyle.State_Selected: painter.fillRect(option.rect, option.palette.highlight()) # Determinar el tipo de línea line_data = self.line_types[index.row()] line_name = line_data["name"] line_style = line_data["style"] # Área de texto text_rect = option.rect.adjusted(5, 0, -100, 0) painter.setPen(option.palette.color(QtGui.QPalette.Text)) painter.drawText(text_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, line_name) # Área de previsualización preview_rect = option.rect.adjusted(option.rect.width() - 90, 2, -5, -2) # Dibujar la línea de muestra pen = QtGui.QPen(QtGui.QColor(0, 0, 0), 2) pen.setStyle(line_style) painter.setPen(pen) center_y = preview_rect.center().y() painter.drawLine(preview_rect.left(), center_y, preview_rect.right(), center_y) except Exception as e: print("Error painting line type:", e) ''' painter.save() painter.setRenderHint(QtGui.QPainter.Antialiasing) if index.row() >= len(self.line_types): painter.restore() return line_data = self.line_types[index.row()] line_name = line_data["name"] line_style = line_data.get("style", QtCore.Qt.SolidLine) dash_pattern = line_data.get("pattern", []) # Fondo para selección if option.state & QtGui.QStyle.State_Selected: painter.fillRect(option.rect, option.palette.highlight()) # Áreas de dibujo text_rect = option.rect.adjusted(5, 0, -100, 0) preview_rect = option.rect.adjusted(option.rect.width() - 90, 2, -5, -2) # Texto painter.setPen(option.palette.color(QtGui.QPalette.Text)) painter.drawText(text_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, line_name) # Línea personalizada pen = QtGui.QPen(QtGui.QColor(0, 0, 0), 2) pen.setStyle(line_style) if line_style == QtCore.Qt.CustomDashLine and dash_pattern: pen.setDashPattern(dash_pattern) painter.setPen(pen) painter.drawLine(preview_rect.left(), preview_rect.center().y(), preview_rect.right(), preview_rect.center().y()) painter.restore()''' def sizeHint(self, option, index): return QtCore.QSize(200, 25) class LineTypeComboBox(QtWidgets.QComboBox): def __init__(self, parent=None): super().__init__(parent) self._delegate = LineTypePreviewDelegate(self) self.setItemDelegate(self._delegate) self.addItems(["Continuous", "Dashed", "Dotted", "Dash-Dot", "Dash-Dot-Dot"]) def paintEvent(self, event): """Pintado simplificado sin QPainter complejo""" painter = QtGui.QPainter(self) try: # Dibujar fondo option = QtWidgets.QStyleOptionComboBox() self.initStyleOption(option) self.style().drawComplexControl(QtGui.QStyle.CC_ComboBox, option, painter, self) # Dibujar texto text_rect = self.rect().adjusted(5, 0, -30, 0) painter.setPen(self.palette().color(QtGui.QPalette.Text)) painter.drawText(text_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, self.currentText()) finally: painter.end() # Asegurar que el painter se cierre correctamente layers = [ ("Available area", QtGui.QColor(0, 204, 153), "Continuous", "1", True), ("Available area Names", QtGui.QColor(255, 255, 255), "Continuous", "1", True), ("Areas Exclusion", QtGui.QColor(255, 85, 0), "Continuous", "1", True), ("Areas Exclusion Offset", QtGui.QColor(255, 85, 0), "Continuous", "1", True), ("Areas Exclusion Name", QtGui.QColor(255, 85, 0), "Continuous", "1", True), ("Areas Cadastral Plot", QtGui.QColor(255, 255, 255), "Continuous", "1", True), ("Areas Cadastral Plot Name", QtGui.QColor(255, 255, 255), "Continuous", "1", True), ("Areas Offset", QtGui.QColor(255, 255, 255), "Continuous", "1", True), ("Cable codes LV AC inverter", QtGui.QColor(255, 255, 255), "Continuous", "1", True), ("Cable codes LV string", QtGui.QColor(255, 255, 255), "Continuous", "1", True), ("Cable codes MV System", QtGui.QColor(255, 255, 255), "Continuous", "1", True), ("CABLES LV AC inverter 120 mm2", QtGui.QColor(255, 204, 0), "Continuous", "1", True), ("CABLES LV AC inverter 185 mm2", QtGui.QColor(204, 153, 0), "Continuous", "1", True), ("CABLES LV string 4 mm2", QtGui.QColor(255, 255, 0), "Continuous", "1", True), ("CABLES LV string 10 mm2", QtGui.QColor(255, 255, 102), "Continuous", "1", True), ("CABLES MV system 300 mm2", QtGui.QColor(102, 51, 0), "Continuous", "1", True), ("CIVIL Fence", QtGui.QColor(102, 102, 102), "FENCELINE1", "1", True), ("CIVIL External Roads", QtGui.QColor(91, 91, 91), "Continuous", "1", True), ("CIVIL External Roads Axis", QtGui.QColor(255, 255, 192), "Dashed", "1", True), ("CIVIL External Roads Text", QtGui.QColor(255, 255, 192), "Continuous", "1", True), ("CIVIL Internal Roads", QtGui.QColor(153, 95, 76), "Continuous", "1", True), ("CIVIL Internal Roads Axis", QtGui.QColor(192, 192, 192), "Dashed", "1", True), ("CIVIL External Roads Text", QtGui.QColor(192, 192, 192), "Continuous", "1", True), ("Contour Line Legend text", QtGui.QColor(255, 255, 255), "Continuous", "1", True), ("Major contour line", QtGui.QColor(0, 0, 0), "Continuous", "1", True), ("Major contour value", QtGui.QColor(0, 0, 0), "Continuous", "1", True), ("Minor contour line", QtGui.QColor(128, 128, 128), "Continuous", "1", True), ("Minor contour value", QtGui.QColor(128, 128, 128), "Continuous", "1", True), ("Power Stations", QtGui.QColor(255, 0, 0), "Continuous", "1", True), ("Power Stations Names", QtGui.QColor(255, 255, 255), "Continuous", "1", True), ("ST", QtGui.QColor(255, 255, 255), "Continuous", "1", True), ("ST Names", QtGui.QColor(255, 255, 0), "Continuous", "1", True), ("String Inv", QtGui.QColor(255, 255, 255), "Continuous", "1", True), ("STRUC Structure 1", QtGui.QColor(0, 0, 255), "Continuous", "1", True), ("STRUC Structure 2", QtGui.QColor(0, 0, 204), "Continuous", "1", True), ("STRUC Structure 3", QtGui.QColor(0, 0, 153), "Continuous", "1", True), ("STRUC Structure 4", QtGui.QColor(0, 0, 128), "Continuous", "1", True), ("STRUC Structure 5", QtGui.QColor(0, 0, 102), "Continuous", "1", True), ("STRUC Structure 6", QtGui.QColor(0, 0, 76), "Continuous", "1", True), ("STRUC Structure 7", QtGui.QColor(0, 0, 51), "Continuous", "1", True), ("STRUC Structure 8", QtGui.QColor(0, 0, 25), "Continuous", "1", True), ("Structure Codes", QtGui.QColor(255, 255, 255), "Continuous", "1", True), ("TRENCHES Low voltage 400.0 x 1000.0 m", QtGui.QColor(128, 128, 128), "Continuous", "1", True), ("TRENCHES Medium voltage 400.0 x 1000.", QtGui.QColor(255, 255, 255), "Continuous", "1", True), ("TRENCHES Medium voltage 800.0 x 1000.", QtGui.QColor(255, 255, 255), "Continuous", "1", True), ("TRENCHES Medium voltage 800.0 x 1500.", QtGui.QColor(255, 255, 255), "Continuous", "1", True), ] class _PVPlantExportDXF(QtGui.QWidget): '''The editmode TaskPanel to select what you want to export''' def __init__(self): import os QtGui.QWidget.__init__(self) # self.form: self.form = FreeCADGui.PySideUic.loadUi( os.path.join(os.path.dirname(os.path.realpath(__file__)), "exportDXF.ui")) # setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "convert.svg"))) # self.form.buttonTo.clicked.connect(self.addTo) self.layout = QtGui.QHBoxLayout(self) self.layout.setContentsMargins(4, 4, 4, 4) self.layout.addWidget(self.form) self.form.buttonAcept.clicked.connect(self.onAceptClick) self.form.buttonCancel.clicked.connect(self.onCancelClick) self.delegate = LineTypeEditorDelegate() self.form.tableLayers.setItemDelegateForColumn(3, self.delegate) from datetime import datetime import os output_dir = os.path.join(os.path.dirname(FreeCAD.ActiveDocument.FileName), "outputs", "autocad") os.makedirs(output_dir, exist_ok=True) timestamp = datetime.now().strftime("%Y%m%d%H%M%S") self.filename = os.path.join(output_dir, f"{timestamp}-{FreeCAD.ActiveDocument.Name}.dxf") # Limpiar la tabla for row in range(self.form.tableLayers.rowCount() - 1, -1, -1): self.form.tableLayers.removeRow(row) # Configuración de las capas por defecto for row in layers: self.add_row(*row) def save_project_settings(self): """Guarda la configuración actual en el proyecto de FreeCAD""" try: # Guardar estado de la tabla table_data = [] for row in range(self.form.tableLayers.rowCount()): # Estado del checkbox checked = self.form.tableLayers.cellWidget(row, 0).findChild(QtWidgets.QCheckBox).isChecked() # Nombre de capa name = self.form.tableLayers.item(row, 1).text() # Color color_btn = self.form.tableLayers.cellWidget(row, 2).findChild(QtWidgets.QPushButton) color = color_btn.color color_str = f"{color.red()},{color.green()},{color.blue()}" # Tipo de línea line_type = self.form.tableLayers.item(row, 3).text() # Grosor thickness_combo = self.form.tableLayers.cellWidget(row, 4) thickness = thickness_combo.currentText() table_data.append({ "name": name, "color": color_str, "line_type": line_type, "thickness": thickness, "checked": checked }) # Crear o actualizar el objeto de configuración en el proyecto config_obj = None for obj in FreeCAD.ActiveDocument.Objects: if obj.Name == "DXF_Export_Config": config_obj = obj break if not config_obj: config_obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython", "DXF_Export_Config") # Guardar como propiedad del objeto config_obj.addProperty("App::PropertyString", "TableSettings", "DXF Export", "Table configuration") config_obj.TableSettings = json.dumps(table_data) print("Configuración guardada en el proyecto") except Exception as e: print("Error guardando configuración en el proyecto:", e) def load_project_settings(self): """Carga la configuración guardada desde el proyecto de FreeCAD""" try: # Buscar objeto de configuración en el proyecto config_obj = None for obj in FreeCAD.ActiveDocument.Objects: if obj.Name == "DXF_Export_Config": config_obj = obj break if config_obj and hasattr(config_obj, "TableSettings"): table_json = config_obj.TableSettings if table_json: table_data = json.loads(table_json) self.load_table_data(table_data) return # Si no hay configuración guardada para este proyecto, cargar configuración por defecto self.add_default_rows() except Exception as e: print("Error cargando configuración del proyecto:", e) self.add_default_rows() def add_default_rows(self): """Añade filas por defecto cuando no hay configuración guardada""" self.add_row("Areas_Boundary", QtGui.QColor(0, 125, 125), "FENCELINE1", "1", True) self.add_row("Areas_Exclusion", QtGui.QColor(255, 0, 0), "DASHED", "1", True) self.add_row("Internal_Roads", QtGui.QColor(128, 128, 128), "CONTINUOUS", "1", True) self.add_row("Frames", QtGui.QColor(0, 255, 0), "CONTINUOUS", "1", True) def load_table_data(self, table_data): """Carga los datos en la tabla desde una estructura de datos""" for layer_data in table_data: # Parsear color color_parts = layer_data["color"].split(",") if len(color_parts) == 3: color = QtGui.QColor(int(color_parts[0]), int(color_parts[1]), int(color_parts[2])) else: color = QtGui.QColor(0, 0, 0) # Color por defecto si hay error # Añadir fila self.add_row( layer_data["name"], color, layer_data["line_type"], layer_data["thickness"], layer_data["checked"] ) def add_row(self, name, color, line_type, thickness, checked=True): row = self.form.tableLayers.rowCount() self.form.tableLayers.insertRow(row) self.form.tableLayers.setRowHeight(row, 25) # Checkbox checkbox = QtWidgets.QCheckBox() checkbox.setChecked(checked) cell_widget = QtWidgets.QWidget() layout = QtWidgets.QHBoxLayout(cell_widget) layout.addWidget(checkbox) layout.setAlignment(QtCore.Qt.AlignCenter) layout.setContentsMargins(0, 0, 0, 0) self.form.tableLayers.setCellWidget(row, 0, cell_widget) # Nombre de capa item = QtWidgets.QTableWidgetItem(name) item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable) self.form.tableLayers.setItem(row, 1, item) # Selector de color color_btn = QtWidgets.QPushButton() color_btn.setFixedSize(20, 20) color_btn.setStyleSheet(f"background-color: {color.name()}; border: 1px solid #808080; border-radius: 3px;") color_btn.color = color color_btn.clicked.connect(lambda: self.change_color(color_btn)) cell_widget = QtWidgets.QWidget() layout = QtWidgets.QHBoxLayout(cell_widget) layout.addWidget(color_btn) layout.setAlignment(QtCore.Qt.AlignCenter) layout.setContentsMargins(0, 0, 0, 0) self.form.tableLayers.setCellWidget(row, 2, cell_widget) # Tipo de línea item = QtWidgets.QTableWidgetItem(line_type) self.form.tableLayers.setItem(row, 3, item) # Grosor de línea thickness_combo = QtWidgets.QComboBox() thickness_combo.addItems(["0.1", "0.2", "0.3", "0.5", "1.0", "2.0"]) thickness_combo.setCurrentText(thickness) self.form.tableLayers.setCellWidget(row, 4, thickness_combo) def change_color(self, button): color = QtWidgets.QColorDialog.getColor(button.color, self, "Seleccionar color") if color.isValid(): button.color = color button.setStyleSheet(f"background-color: {color.name()}; border: 1px solid #808080; border-radius: 3px;") def createLayers(self, exporter): for row in range(self.form.tableLayers.rowCount()): if self.form.tableLayers.cellWidget(row, 0).findChild(QtWidgets.QCheckBox).isChecked(): layer_name = self.form.tableLayers.item(row, 1).text() color = self.form.tableLayers.cellWidget(row, 2).findChild(QtWidgets.QPushButton).color #line_type = self.form.tableLayers.cellWidget(row, 3).currentText() line_type = self.form.tableLayers.item(row, 3).text() exporter.createLayer( layerName=layer_name, layerColor=(color.red(), color.green(), color.blue()), layerLineType=line_type.upper() ) def writeArea(self, exporter): exporter.createPolyline(FreeCAD.ActiveDocument.Site.Boundary, "Available area") areas_types = [("Boundaries", "Available area"), ("CadastralPlots", "Areas Cadastral Plot"), ("Exclusions", "Areas Exclusion"), ("Offsets", "Areas Offset")] for area_type in areas_types: if hasattr(FreeCAD.ActiveDocument, area_type[0]): area_group = FreeCAD.ActiveDocument.getObjectsByLabel(area_type[0]) if len(area_group): for area in area_group[0].Group: tmp = exporter.createPolyline(area, area_type[1]) if area_type[0] == "Exclusions": exporter.createHatch( tmp, pattern="ANSI37", scale=0.3, angle=0, layer=area_type[1] ) def writeFrameSetups(self, exporter): if not hasattr(FreeCAD.ActiveDocument, "Site"): return # 2. Postes for ts in FreeCAD.ActiveDocument.Site.Frames: for poletype in ts.PoleType: block = exporter.createBlock(poletype.Label) w = poletype.Base.Shape.Wires[0] center = w.BoundBox.Center w.Placement.Base = w.Placement.Base.sub(center) block.add_lwpolyline(getWire(w)) block.add_circle((0, 0), 0.2, dxfattribs={'layer': 'Structure Posts'}) #'color': 2, p = math.sin(math.radians(45)) * 0.2 block.add_line((-p, -p), (p, p), dxfattribs={'layer': 'Structure Posts'}) block.add_line((-p, p), (p, -p), dxfattribs={'layer': 'Structure Posts'}) # 2. Frames for ts in FreeCAD.ActiveDocument.Site.Frames: w = max(ts.Shape.SubShapes[0].SubShapes[0].SubShapes[0].Faces, key=lambda x: x.Area) pts = [w.BoundBox.getPoint(i) for i in range(4)] pts.append(FreeCAD.Vector(pts[0])) w = Part.makePolygon(pts) w.Placement.Base = w.Placement.Base.sub(w.BoundBox.Center) mblockname = "Trina_TSM-DEG21C-20-6XXWp Vertex" mblock = exporter.createBlock(mblockname) mblock.add_lwpolyline(getWire(w)) rblock = exporter.createBlock(ts.Label) w = max(ts.Shape.SubShapes[0].SubShapes[1].SubShapes[0].Faces, key=lambda x: x.Placement.Base.z).Wires[0] w.Placement.Base = w.Placement.Base.sub(w.BoundBox.Center) rblock.add_lwpolyline(getWire(w)) for module in ts.Shape.SubShapes[0].SubShapes[0].SubShapes: point = FreeCAD.Vector(module.BoundBox.Center) * 0.001 point = point[:2] rblock.add_blockref(mblockname, point, dxfattribs={ 'xscale': .0, 'yscale': .0, 'rotation': 0}) for ind in range(int(ts.NumberPole.Value)): point = ts.Shape.SubShapes[1].SubShapes[0].SubShapes[ind].Placement.Base * 0.001 point = point[:2] name = ts.PoleType[ts.PoleSequence[ind]].Label rblock.add_blockref(name, point, dxfattribs={ 'xscale': .0, 'yscale': .0, 'rotation': 0}) def writeFrames(self, exporter): objects = findObjects('Tracker') for frame in objects: if hasattr(frame, "Setup"): point = frame.Placement.Base * 0.001 point = point[:2] exporter.insertBlock(frame.Setup.Label, point=point, rotation=frame.AngleZ) def writeRoads(self, exporter): objects = findObjects("Road") #rblock = exporter.createBlock("Internal_roads") for road in objects: base = exporter.createPolyline(road.Base, "Internal_Roads") base.dxf.const_width = road.Width.Value * 0.001 axis = exporter.createPolyline(road.Base, "Internal_Roads_Axis") axis.dxf.const_width = .2 if FreeCAD.ActiveDocument.Transport: for road in FreeCAD.ActiveDocument.Transport.Group: base = exporter.createPolyline(road, "CIVIL External Roads") base.dxf.const_width = road.Width axis = exporter.createPolyline(road, "CIVIL External Roads Axis") axis.dxf.const_width = .2 def writeTrenches(self, exporter): objects = findObjects("Trench") # rblock = exporter.createBlock("Internal_roads") for obj in objects: base = exporter.createPolyline(obj.Base, "Trench") base.dxf.const_width = obj.Width.Value * 0.001 def setup_layout4(self, doc): layout2 = doc.layouts.new("scale 1-1") # The default paperspace scale is 1:1 # 1 mm printed is 1 drawing unit in paperspace # For most use cases this is the preferred scaling and important fact: # the paperspace scaling has no influence on the VIEWPORT scaling - this is # a total different topic, see example "viewports_in_paperspace.py" layout2.page_setup(size=(297, 210), margins=(10, 10, 10, 10), units="mm", scale=(1, 1), #offset=(50, 50), ) layout2.add_viewport( # center of viewport in paperspace units center=(100, 100), # viewport size in paperspace units size=(50, 50), # modelspace point to show in center of viewport in WCS view_center_point=(60, 40), # how much modelspace area to show in viewport in drawing units view_height=20, #status=2, ) lower_left, upper_right = layout2.get_paper_limits() x1, y1 = lower_left x2, y2 = upper_right center = lower_left.lerp(upper_right) # Add DXF entities to the "Layout1" in paperspace coordinates: layout2.add_line((x1, center.y), (x2, center.y)) # horizontal center line layout2.add_line((center.x, y1), (center.x, y2)) # vertical center line layout2.add_circle((0, 0), radius=5) # plot origin def createPaperSpaces(self, exporter): # Datos para el cajetín title_block_data = { 'width': 190, 'height': 50, 'fields': [ {'name': 'Proyecto', 'value': FreeCAD.ActiveDocument.Label}, {'name': 'Escala', 'value': '1:100'}, {'name': 'Fecha', 'value': QtCore.QDate.currentDate().toString("dd/MM/yyyy")}, {'name': 'Dibujado por', 'value': os.getlogin()} ] } # Datos para la tabla de información table_data = { 'Potencia pico': '50 MWp', 'Número de trackers': str(len(findObjects('Tracker'))), 'Superficie total': f"{FreeCAD.ActiveDocument.Site.Boundary.Shape.Area / 10000:.2f} ha" } # Datos para el viewport viewport_data = { 'center': (150, 100), 'size': (250, 180), 'view_center': (0, 0), 'view_height': 200 } # Crear diferentes layouts para diferentes tamaños de papel exporter.createPaperSpaceLayout( "Layout_A4", (297, 210), # A4 landscape title_block_data, table_data, viewport_data ) exporter.createPaperSpaceLayout( "Layout_A3", (420, 297), # A3 landscape title_block_data, table_data, viewport_data ) def onAceptClick(self): exporter = exportDXF(self.filename) exporter.createFile() # Crear capas configuradas self.createLayers(exporter) # Exportar objetos self.writeArea(exporter) self.writeFrameSetups(exporter) self.writeFrames(exporter) self.writeRoads(exporter) self.writeTrenches(exporter) # Crear espacios de papel #self.setup_layout4(exporter.doc) #self.createPaperSpaces(exporter) if hasattr(FreeCAD.ActiveDocument, "Background"): # Exportar como imagen en lugar de polilínea exporter.export_feature_image(FreeCAD.ActiveDocument.Background, "Site_Image") # Guardar archivo exporter.save() # Mostrar mensaje de éxito QtWidgets.QMessageBox.information( self, "Exportación completada", f"Archivo DXF guardado en:\n{self.filename}" ) self.close() def onCancelClick(self): # Cerrar el diálogo sin hacer nada self.close() class CommandExportDXF: def GetResources(self): return {'Pixmap': str(os.path.join(DirIcons, "dxf.svg")), 'Accel': "E, A", 'MenuText': "Export to DXF", 'ToolTip': QT_TRANSLATE_NOOP("Placement", "Export choosed layers to dxf")} def Activated(self): taskd = _PVPlantExportDXF() taskd.setParent(FreeCADGui.getMainWindow()) taskd.setWindowFlags(QtCore.Qt.Dialog or QtCore.Qt.Dialog) taskd.setWindowModality(QtCore.Qt.WindowModal) taskd.show() def IsActive(self): if FreeCAD.ActiveDocument: return True else: return False '''if FreeCAD.GuiUp: FreeCADGui.addCommand('exportDXF', _CommandExportDXF())'''