import math import FreeCAD 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} 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 = [ ("Continuous", QtCore.Qt.SolidLine), ("Dashed", QtCore.Qt.DashLine), ("Dotted", QtCore.Qt.DotLine), ("Dash-Dot", QtCore.Qt.DashDotLine), ("Dash-Dot-Dot", QtCore.Qt.DashDotDotLine) ]''' 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): 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): painter = QtGui.QPainter(self) painter.setRenderHint(QtGui.QPainter.Antialiasing) # Dibujar el fondo del combobox option = QtWidgets.QStyleOptionComboBox() self.initStyleOption(option) self.style().drawComplexControl(QtGui.QStyle.CC_ComboBox, option, painter, self) # Obtener el texto y estilo actual current_text = self.currentText() line_style = next( (style for name, style in self._delegate.line_types if name == current_text), QtCore.Qt.SolidLine ) # Área de dibujo text_rect = self.rect().adjusted(5, 0, -30, 0) preview_rect = self.rect().adjusted(self.width() - 70, 2, -5, -2) # Dibujar texto painter.setPen(self.palette().color(QtGui.QPalette.Text)) painter.drawText(text_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, current_text) # Dibujar línea de previsualización 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) class exportDXF: def __init__(self, filename): ''' ''' self.doc = None self.msp = None self.filename = filename ''' 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() print("linetypes: ", self.doc.linetypes) 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): try: data = getWire(wire.Shape) lwp = self.msp.add_lwpolyline(data) return lwp except Exception as e: print("Error creating polyline:", e) return None 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 Part 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)) class _PVPlantExportDXF(QtGui.QWidget): '''The editmode TaskPanel to select what you want to export''' def __init__(self): # super(_PVPlantExportDXF, self).__init__() QtGui.QWidget.__init__(self) import os from datetime import datetime # 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.form.tableLayers.setItemDelegateForColumn(3, LineTypeDelegate()) 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.add_row("Layer 1", QtGui.QColor(255, 0, 0), "Continua", "1") self.add_row("Layer 2", QtGui.QColor(255, 0, 0), "Continua", "1") path = os.path.join(os.path.dirname(FreeCAD.ActiveDocument.FileName), "outputs", "autocad") if not os.path.exists(path): os.makedirs(path) name = datetime.now().strftime("%Y%m%d%H%M%S") + "-" + FreeCAD.ActiveDocument.Name self.filename = os.path.join(path, name) + ".dxf" def add_row(self, name, color, line_type, thickness): row = self.form.tableLayers.rowCount() self.form.tableLayers.insertRow(row) self.form.tableLayers.setRowHeight(row, 20) # Columna 0: Checkbox checkbox = QtWidgets.QCheckBox() 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) # Columna 1: Nombre (editable) item = QtWidgets.QTableWidgetItem(name) item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable) self.form.tableLayers.setItem(row, 1, item) # Columna 2: Selector de color color_btn = QtWidgets.QPushButton() color_btn.setFixedSize(20, 20) # Tamaño del cuadrado color_btn.setStyleSheet(f""" QPushButton {{ background-color: {color.name()}; border: 1px solid #808080; border-radius: 3px; }} QPushButton:hover {{ border: 2px solid #606060; }} """) color_btn.color = color # Almacenar el color como atributo color_btn.clicked.connect(lambda: self.change_color(color_btn)) # Widget contenedor para centrar el botón 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) # Columna 3: Tipo de línea (combobox) line_combo = LineTypeComboBox() line_combo.addItems(["Continua", "Discontinua", "Punteada", "Mixta"]) line_combo.setCurrentText(line_type) self.form.tableLayers.setCellWidget(row, 3, line_combo) # Columna 4: Grosor de línea (combobox) thickness_combo = QtWidgets.QComboBox() thickness_combo.addItems(["1", "2", "3", "4"]) 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()}") def createLayers(self): ''' ''' self.exporter.createLayer("Areas_Boundary", layerColor=(0, 125, 125), layerLineType="FENCELINE1") self.exporter.createLayer("Areas_Exclusion", layerColor=(255, 0, 0)) self.exporter.createLayer("Areas_Offsets", layerColor=(128, 128, 255)) self.exporter.createLayer("Internal_Roads", layerColor=(128, 128, 128)) self.exporter.createLayer("Internal_Roads_Axis", layerColor=(255, 255, 255), layerLineType="DASHEDX2") def writeArea(self): pol = self.exporter.createPolyline(FreeCAD.ActiveDocument.Site.Boundary) if pol: pol.dxf.layer = "boundary" for area in FreeCAD.ActiveDocument.Boundaries.Group: pol = self.exporter.createPolyline(area) pol.dxf.layer = "Areas_Boundary" for area in FreeCAD.ActiveDocument.Exclusions.Group: pol = self.exporter.createPolyline(area) pol.dxf.layer = "Areas_Exclusion" for area in FreeCAD.ActiveDocument.Offsets.Group: pol = self.exporter.createPolyline(area) pol.dxf.layer = "Areas_Offsets" def writeFrameSetups(self): import Part # 1. Profiles: profilelist = list() for ts in FreeCAD.ActiveDocument.Site.Frames: for poletype in ts.PoleType: if not (poletype in profilelist): profilelist.append(poletype) block = self.exporter.createBlock(poletype.Label) w = poletype.Base.Shape.Wires[0] w.Placement.Base = FreeCAD.Vector(w.Placement.Base).sub(w.BoundBox.Center) block.add_lwpolyline(getWire(w)) block.add_circle((0, 0), 0.2, dxfattribs={'color': 2}) p = math.sin(math.radians(45)) * 0.2 block.add_line((-p, -p), (p, p), dxfattribs={"layer": "MyLines"}) block.add_line((-p, p), (p, -p), dxfattribs={"layer": "MyLines"}) # 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 = self.exporter.createBlock(mblockname) mblock.add_lwpolyline(getWire(w)) rblock = self.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): objects = findObjects('Tracker') for frame in objects: if hasattr(frame, "Setup"): point = frame.Placement.Base * 0.001 point = point[:2] self.exporter.insertBlock(frame.Setup.Label, point=point, rotation=frame.AngleZ) def writeRoads(self): objects = findObjects("Road") #rblock = self.exporter.createBlock("Internal_roads") for road in objects: base = self.exporter.createPolyline(road.Base) base.dxf.const_width = road.Width.Value * 0.001 base.dxf.layer = "Internal_Roads" axis = self.exporter.createPolyline(road.Base) axis.dxf.const_width = .2 axis.dxf.layer = "Internal_Roads_Axis" #my_lines = doc.layers.get('MyLines') def writeTrenches(self): objects = findObjects("Trench") # rblock = self.exporter.createBlock("Internal_roads") for obj in objects: base = self.exporter.createPolyline(obj.Base) base.dxf.const_width = obj.Width.Value * 0.001 base.dxf.layer = "Trench" 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 onAceptClick(self): self.exporter = exportDXF(self.filename) if self.exporter: self.exporter.createFile() self.createLayers() self.writeArea() self.writeFrameSetups() self.writeFrames() self.writeRoads() self.writeTrenches() self.setup_layout4(self.exporter.doc) self.exporter.save() print(self.filename) 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())'''