Files
PVPlant/Export/exportDXF.py
2025-07-06 01:12:08 +02:00

1021 lines
39 KiB
Python

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}
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):
try:
data = getWire(wire.Shape)
lwp = self.msp.add_lwpolyline(data)
return lwp
except Exception as e:
print("Error creating polyline:", e)
return None
# =================================================================================
# 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
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
self.add_row("Areas_Boundary", QtGui.QColor(0, 125, 125), "FENCELINE1", "1", True)
self.add_row("Areas_Exclusion", QtGui.QColor(255, 0, 0), "CONTINUOUS", "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 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, "boundary")
areas_types = ["Boundaries", "Exclusions", "Offsets"]
for area_type in areas_types:
if hasattr(FreeCAD.ActiveDocument, area_type):
for area in FreeCAD.ActiveDocument.Boundaries.Group:
exporter.createPolyline(area, "Areas_Boundary")
'''for area in FreeCAD.ActiveDocument.Boundaries.Group:
pol = exporter.createPolyline(area)
pol.dxf.layer = "Areas_Boundary"
for area in FreeCAD.ActiveDocument.Exclusions.Group:
pol = exporter.createPolyline(area)
pol.dxf.layer = "Areas_Exclusion"
for area in FreeCAD.ActiveDocument.Offsets.Group:
pol = exporter.createPolyline(area)
pol.dxf.layer = "Areas_Offsets"'''
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={'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 = 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, "External_Roads")
base.dxf.const_width = road.Width
axis = exporter.createPolyline(road.Base, "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)
# 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())'''