From 049898c93918406090499d3a98d04f6b31557c08 Mon Sep 17 00:00:00 2001 From: Javi Date: Sun, 17 Aug 2025 13:34:09 +0400 Subject: [PATCH] updates --- Electrical/group.py | 46 ++-- Export/exportDXF.py | 1 + PVPlantPlacement.py | 256 ++++++++++++++++----- PVPlantTools.py | 24 -- cidownloader.py => Plugins/cidownloader.py | 0 Utils/PVPlantUtils.py | 10 +- 6 files changed, 233 insertions(+), 104 deletions(-) rename cidownloader.py => Plugins/cidownloader.py (100%) diff --git a/Electrical/group.py b/Electrical/group.py index 2721694..2d6919a 100644 --- a/Electrical/group.py +++ b/Electrical/group.py @@ -141,29 +141,31 @@ def groupTrackersToTransformers(transformer_power, max_distance): for i, group in enumerate(transformer_groups): # Crear la esfera que representará el CT - ct_sphere = FreeCAD.ActiveDocument.addObject("Part::Sphere", f"CT_{i + 1}") - ct_sphere.Radius = 5000 # 2m de radio - ct_sphere.Placement.Base = FreeCAD.Vector(group['center'][0], group['center'][1], 0) + ct_shape = FreeCAD.ActiveDocument.addObject("Part::Box", f"CT_{i + 1}") + ct_shape.Length = 6058 + ct_shape.Width = 2438 + ct_shape.Height = 2591 + ct_shape.Placement.Base = FreeCAD.Vector(group['center'][0], group['center'][1], 0) # Añadir propiedades personalizadas - ct_sphere.addProperty("App::PropertyLinkList", "Trackers", "CT", + ct_shape.addProperty("App::PropertyLinkList", "Trackers", "CT", "Lista de trackers asociados a este CT") - ct_sphere.addProperty("App::PropertyFloat", "TotalPower", "CT", + ct_shape.addProperty("App::PropertyFloat", "TotalPower", "CT", "Potencia total del grupo (W)") - ct_sphere.addProperty("App::PropertyFloat", "NominalPower", "CT", + ct_shape.addProperty("App::PropertyFloat", "NominalPower", "CT", "Potencia nominal del transformador (W)") - ct_sphere.addProperty("App::PropertyFloat", "Utilization", "CT", + ct_shape.addProperty("App::PropertyFloat", "Utilization", "CT", "Porcentaje de utilización (Total/Nominal)") # Establecer valores de las propiedades - ct_sphere.Trackers = group['trackers'] - ct_sphere.TotalPower = group['total_power'].Value - ct_sphere.NominalPower = transformer_power - ct_sphere.Utilization = (group['total_power'].Value / transformer_power) * 100 + ct_shape.Trackers = group['trackers'] + ct_shape.TotalPower = group['total_power'].Value + ct_shape.NominalPower = transformer_power + ct_shape.Utilization = (group['total_power'].Value / transformer_power) * 100 # Configurar visualización # Calcular color basado en utilización (verde < 100%, amarillo < 110%, rojo > 110%) - utilization = ct_sphere.Utilization + utilization = ct_shape.Utilization if utilization <= 100: color = (0.0, 1.0, 0.0) # Verde elif utilization <= 110: @@ -171,15 +173,15 @@ def groupTrackersToTransformers(transformer_power, max_distance): else: color = (1.0, 0.0, 0.0) # Rojo - ct_sphere.ViewObject.ShapeColor = color - ct_sphere.ViewObject.Transparency = 40 # 40% de transparencia + ct_shape.ViewObject.ShapeColor = color + ct_shape.ViewObject.Transparency = 40 # 40% de transparencia # Añadir etiqueta con información - ct_sphere.ViewObject.DisplayMode = "Shaded" - ct_sphere.Label = f"CT {i + 1} ({ct_sphere.TotalPower / 1000:.1f}kW/{ct_sphere.NominalPower / 1000:.1f}kW)" + ct_shape.ViewObject.DisplayMode = "Shaded" + ct_shape.Label = f"CT {i + 1} ({ct_shape.TotalPower / 1000:.1f}kW/{ct_shape.NominalPower / 1000:.1f}kW)" # Añadir al grupo principal - transformer_group.addObject(ct_sphere) + transformer_group.addObject(ct_shape) FreeCAD.Console.PrintMessage(f"Se crearon {len(transformer_groups)} centros de transformación\n") onSelectGatePoint() @@ -195,7 +197,7 @@ class InternalPathCreator: self.gate_point = gate_point self.strategy = strategy self.path_width = path_width - self.ct_spheres = [] + self.ct_shapes = [] self.ct_positions = [] def get_transformers(self): @@ -204,13 +206,13 @@ class InternalPathCreator: FreeCAD.Console.PrintError("No se encontró el grupo 'Transformers'\n") return False - self.ct_spheres = transformers_group.Group - if not self.ct_spheres: + self.ct_shapes = transformers_group.Group + if not self.ct_shapes: FreeCAD.Console.PrintWarning("No hay Centros de Transformación en el grupo\n") return False # Obtener las posiciones de los CTs - for sphere in self.ct_spheres: + for sphere in self.ct_shapes: base = sphere.Placement.Base self.ct_positions.append(FreeCAD.Vector(base.x, base.y, 0)) return True @@ -263,6 +265,8 @@ class InternalPathCreator: y_proj = slope * x_proj + intercept return FreeCAD.Vector(x_proj, y_proj, 0) + # return slope * x + intercept --> desde placement + projected_points = [project_point(p) for p in all_points] # Calcular distancias a lo largo de la línea diff --git a/Export/exportDXF.py b/Export/exportDXF.py index 68f4b6f..a46e74e 100644 --- a/Export/exportDXF.py +++ b/Export/exportDXF.py @@ -626,6 +626,7 @@ layers = [ ("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), diff --git a/PVPlantPlacement.py b/PVPlantPlacement.py index 04e1a1f..b7b0211 100644 --- a/PVPlantPlacement.py +++ b/PVPlantPlacement.py @@ -102,7 +102,7 @@ class _PVPlantPlacementTaskPanel: def createFrameFromPoints(self, dataframe): from Mechanical.Frame import PVPlantFrame - try: + '''try: MechanicalGroup = FreeCAD.ActiveDocument.Frames except: MechanicalGroup = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'Frames') @@ -110,14 +110,41 @@ class _PVPlantPlacementTaskPanel: FreeCAD.ActiveDocument.MechanicalGroup.addObject(MechanicalGroup) if self.form.cbSubfolders.isChecked: - label = "Frames-" + self.PVArea.Label - if label in [obj.Label for obj in FreeCAD.ActiveDocument.Frames.Group]: - MechanicalGroup = FreeCAD.ActiveDocument.getObject(label)[0] + name = "Frames-" + self.PVArea.Label + if name in [obj.Name for obj in FreeCAD.ActiveDocument.Frames.Group]: + MechanicalGroup = FreeCAD.ActiveDocument.getObject(name)[0] else: - group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", label) - group.Label = label + group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", name) + group.Label = name MechanicalGroup.addObject(group) - MechanicalGroup = group + MechanicalGroup = group''' + + doc = FreeCAD.ActiveDocument + + # 1. Obtener o crear el grupo principal 'Frames' + main_group_name = "Frames" + main_group = doc.getObject(main_group_name) + if not main_group: + main_group = doc.addObject("App::DocumentObjectGroup", main_group_name) + main_group.Label = main_group_name + # Asumiendo que existe un grupo 'MechanicalGroup' + if hasattr(doc, 'MechanicalGroup'): + doc.MechanicalGroup.addObject(main_group) + + # 2. Manejar subgrupo si es necesario + group = main_group # Grupo donde se añadirán los marcos + if self.form.cbSubfolders.isChecked(): # ¡Corregido: falta de paréntesis! + subgroup_name = f"Frames-{self.PVArea.Label}" + + # Buscar subgrupo existente + subgroup = next((obj for obj in main_group.Group if obj.Name == subgroup_name), None) + + if not subgroup: + subgroup = doc.addObject("App::DocumentObjectGroup", subgroup_name) + subgroup.Label = subgroup_name + main_group.addObject(subgroup) + group = subgroup + try: placements = dataframe["placement"].tolist() types = dataframe["type"].tolist() @@ -127,7 +154,7 @@ class _PVPlantPlacementTaskPanel: newrack.Label = "Tracker" newrack.Visibility = False newrack.Placement = placements[idx] - MechanicalGroup.addObject(newrack) + group.addObject(newrack) frames.append(newrack) except: placements = dataframe[0] @@ -138,7 +165,7 @@ class _PVPlantPlacementTaskPanel: newrack.Label = "Tracker" newrack.Visibility = False newrack.Placement = idx[1] - MechanicalGroup.addObject(newrack) + groupq.addObject(newrack) frames.append(newrack) if self.PVArea.Name.startswith("FrameArea"): @@ -160,7 +187,7 @@ class _PVPlantPlacementTaskPanel: if exclusion_areas: prohibited_faces = [] for obj in exclusion_areas: - face = self.getProjected(obj.Base.Shape) + face = self.getProjected(obj.Shape.SubShapes[1]) if face.isValid(): prohibited_faces.append(face) self.Area = self.Area.cut(prohibited_faces) @@ -474,64 +501,70 @@ class _PVPlantPlacementTaskPanel: def calculateNonAlignedArray(self): pointsx, pointsy = self.getAligments() + if len(pointsx) == 0: + FreeCAD.Console.PrintWarning("No se encontraron alineaciones X.\n") + return [] footprints = [] for frame in self.FrameSetups: - xx = frame.Length.Value - yy = frame.Width.Value - xx_med = xx / 2 - yy_med = yy / 2 - rec = Part.makePolygon([FreeCAD.Vector(-xx_med, -yy_med, 0), - FreeCAD.Vector(xx_med, -yy_med, 0), - FreeCAD.Vector(xx_med, yy_med, 0), - FreeCAD.Vector(-xx_med, yy_med, 0), - FreeCAD.Vector(-xx_med, -yy_med, 0)]) + l = frame.Length.Value + w = frame.Width.Value + l_med = l / 2 + w_med = w / 2 + rec = Part.makePolygon([FreeCAD.Vector(-l_med, -w_med, 0), + FreeCAD.Vector( l_med, -w_med, 0), + FreeCAD.Vector( l_med, w_med, 0), + FreeCAD.Vector(-l_med, w_med, 0), + FreeCAD.Vector(-l_med, -w_med, 0)]) rec.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0)) footprints.append([frame, rec]) - ref = footprints.pop(0) - xx = ref[0].Length.Value - yy = ref[0].Width.Value - xx_med = xx / 2 - yy_med = yy / 2 - # variables for corridors: - countcols = 0 - countrows = 0 - offsetcols = 0 # ?? - offsetrows = 0 # ?? - valcols = FreeCAD.Units.Quantity(self.form.editColGap.text()).Value - (self.gap_col - yy) + corridor = self.form.groupCorridor.isChecked() + corridor_offset = 0 + count = 0 - pl = [] - for point in pointsx: - p1 = FreeCAD.Vector(point, self.Area.BoundBox.YMax, 0.0) - p2 = FreeCAD.Vector(point, self.Area.BoundBox.YMin, 0.0) + cols = [] + for x in pointsx: + col=[] + x += corridor_offset + p1 = FreeCAD.Vector(x, self.Area.BoundBox.YMax, 0.0) + p2 = FreeCAD.Vector(x, self.Area.BoundBox.YMin, 0.0) line = Part.makePolygon([p1, p2]) - inter = self.Area.section([line]) - pts = [ver.Point for ver in inter.Vertexes] # todo: sort points + pts = [ver.Point for ver in inter.Vertexes] + pts = sorted(pts, key=lambda p: p.y, reverse=True) for i in range(0, len(pts), 2): - line = Part.LineSegment(pts[i], pts[i + 1]) - if line.length() >= ref[1].BoundBox.YLength: - y1 = pts[i].y - ref[1].BoundBox.YLength / 2 - cp = ref[1].copy() - cp.Placement.Base = FreeCAD.Vector(pts[i].x, y1, 0.0) - Part.show(cp) + top = pts[i] + bootom = pts[i + 1] + if top.distanceToPoint(bootom) > footprints[-1][1].BoundBox.YLength: + y1 = top.y - (footprints[-1][1].BoundBox.YLength / 2) + cp = footprints[-1][1].copy() + cp.Placement.Base = FreeCAD.Vector(x + footprints[-1][1].BoundBox.XLength / 2, y1, 0.0) inter = cp.cut([self.Area]) - pts1 = [ver.Point for ver in inter.Vertexes] - if len(pts1) == 0: - continue - y1 = min(pts1, key=lambda p: p.y).y - pointsy = np.arange(y1, pts[i + 1].y, -self.gap_row) - continue - for pointy in pointsy: - cp = ref[1].copy() - cp.Placement.Base = FreeCAD.Vector(pts[i].x + ref[1].BoundBox.XLength / 2, pointy, 0.0) - cut = cp.cut([self.Area], 0) - #print(y1, " - ", pointy, " - ", len(cut.Vertexes)) - #if len(cut.Vertexes) == 0: - Part.show(cp) - pl.append([ref[0], pointy]) - return pl + vtx = [ver.Point for ver in inter.Vertexes] + mod = top.y + if len(vtx) != 0: + mod = min(vtx, key=lambda p: p.y).y + #y1 = cp.Placement.Base.y - mod + + tmp = optimized_cut(mod - bootom.y, [ftp[1].BoundBox.YLength for ftp in footprints], 500, 'greedy') + for opt in tmp[0]: + mod -= (footprints[opt][1].BoundBox.YLength / 2) + pl = FreeCAD.Vector(x + footprints[opt][1].BoundBox.XLength / 2, mod, 0.0) + cp = footprints[opt][1].copy() + if self.isInside(cp, pl): + col.append([footprints[opt][0], pl]) + mod -= ((footprints[opt][1].BoundBox.YLength / 2) + 500) + Part.show(cp) + + if corridor and len(col) > 0: + count += 1 + if count == self.form.editColCount.value(): + corridor_offset += 12000 + count = 0 + + cols.append(cols) + return self.adjustToTerrain(cols) def accept(self): from datetime import datetime @@ -583,6 +616,115 @@ class _PVPlantPlacementTaskPanel: FreeCAD.ActiveDocument.recompute() +def optimized_cut(L_total, piezas, margen=0, metodo='auto'): + """ + Encuentra la combinación óptima de piezas para minimizar el desperdicio, + considerando un margen entre piezas. + + Args: + L_total (int): Longitud total del material. + piezas (list): Lista de longitudes de los patrones de corte. + margen (int): Espacio perdido entre piezas consecutivas. + metodo (str): 'dp' para programación dinámica, 'greedy' para voraz, 'auto' para selección automática. + + Returns: + tuple: (piezas_seleccionadas, desperdicio) + """ + # Filtrar piezas inválidas + piezas = [p for p in piezas if 0 < p <= L_total] + if not piezas: + return [], L_total + + # Transformar longitudes y longitud total con margen + longitudes_aumentadas = [p + margen for p in piezas] + L_total_aumentado = L_total + margen + + # Selección automática de método + if metodo == 'auto': + if L_total_aumentado <= 10000 and len(piezas) <= 100: + metodo = 'dp' + else: + metodo = 'greedy' + + if metodo == 'dp': + n = len(piezas) + dp = [0] * (L_total_aumentado + 1) + parent = [-1] * (L_total_aumentado + 1) # Almacena índices de piezas usadas + + # Llenar la tabla dp y parent + for j in range(1, L_total_aumentado + 1): + for i in range(n): + p_aum = longitudes_aumentadas[i] + if p_aum <= j: + if dp[j] < dp[j - p_aum] + p_aum: + dp[j] = dp[j - p_aum] + p_aum + parent[j] = i # Guardar índice de la pieza + + # Reconstruir solución desde el final + current = L_total_aumentado + seleccion_indices = [] + while current > 0 and parent[current] != -1: + i = parent[current] + seleccion_indices.append(i) + current -= longitudes_aumentadas[i] + + # Calcular desperdicio real + k = len(seleccion_indices) + if k == 0: + desperdicio = L_total + else: + suma_original = sum(piezas[i] for i in seleccion_indices) + desperdicio = L_total - suma_original - margen * (k - 1) + + return seleccion_indices, desperdicio + + elif metodo == 'greedy': + # Crear lista con índices y longitudes aumentadas + lista_con_indices = [(longitudes_aumentadas[i], i) for i in range(len(piezas))] + lista_con_indices.sort(key=lambda x: x[0], reverse=True) # Ordenar descendente + + seleccion_indices = [] + restante = L_total_aumentado + + # Seleccionar piezas vorazmente + for p_aum, i in lista_con_indices: + while restante >= p_aum: + seleccion_indices.append(i) + restante -= p_aum + + # Calcular desperdicio real + k = len(seleccion_indices) + if k == 0: + desperdicio = L_total + else: + suma_original = sum(piezas[i] for i in seleccion_indices) + desperdicio = L_total - suma_original - margen * (k - 1) + + return seleccion_indices, desperdicio + + +# Ejemplo de uso +'''if __name__ == "__main__": + L_total = 100 + piezas = [25, 35, 40, 20, 15, 30, 50] + margen = 5 + + print("Solución óptima con margen (programación dinámica):") + seleccion, desperd = corte_optimizado(L_total, piezas, margen, 'dp') + print(f"Piezas usadas: {seleccion}") + print(f"Margen entre piezas: {margen} cm") + print(f"Material útil: {sum(seleccion)} cm") + print(f"Espacio usado por márgenes: {(len(seleccion) - 1) * margen} cm") + print(f"Desperdicio total: {desperd} cm") + + print("\nSolución aproximada con margen (algoritmo voraz):") + seleccion_g, desperd_g = corte_optimizado(L_total, piezas, margen, 'greedy') + print(f"Piezas usadas: {seleccion_g}") + print(f"Margen entre piezas: {margen} cm") + print(f"Material útil: {sum(seleccion_g)} cm") + print(f"Espacio usado por márgenes: {(len(seleccion_g) - 1) * margen} cm") + print(f"Desperdicio total: {desperd_g} cm")''' + # ---------------------------------------------------------------------------------------------------------------------- # function AdjustToTerrain diff --git a/PVPlantTools.py b/PVPlantTools.py index d46ebe1..8240e0d 100644 --- a/PVPlantTools.py +++ b/PVPlantTools.py @@ -54,30 +54,6 @@ class CommandPVPlantSite: return -'''class CommandPVPlantGeoreferencing: - @staticmethod - def GetResources(): - return {'Pixmap': str(os.path.join(DirIcons, "Location.svg")), - 'Accel': "G, R", - 'MenuText': QT_TRANSLATE_NOOP("Georeferencing","Georeferencing"), - 'ToolTip': QT_TRANSLATE_NOOP("Georeferencing","Referenciar el lugar")} - - @staticmethod - def IsActive(): - if FreeCAD.ActiveDocument: - return True - else: - return False - - @staticmethod - def Activated(): - import PVPlantGeoreferencing - taskd = PVPlantGeoreferencing.MapWindow() - #taskd.setParent(FreeCADGui.getMainWindow()) - #taskd.setWindowFlags(QtCore.Qt.Window) - taskd.show()#exec_()''' - - class CommandProjectSetup: @staticmethod def GetResources(): diff --git a/cidownloader.py b/Plugins/cidownloader.py similarity index 100% rename from cidownloader.py rename to Plugins/cidownloader.py diff --git a/Utils/PVPlantUtils.py b/Utils/PVPlantUtils.py index 2c51fc0..049684d 100644 --- a/Utils/PVPlantUtils.py +++ b/Utils/PVPlantUtils.py @@ -224,14 +224,20 @@ def getProjected(shape, direction=FreeCAD.Vector(0, 0, 1)): # Based on Draft / s return ow -def findObjects(classtype): +'''def findObjects(classtype): objects = FreeCAD.ActiveDocument.Objects objlist = list() for object in objects: if hasattr(object, "Proxy"): if object.Proxy.Type == classtype: objlist.append(object) - return objlist + return objlist''' + +def findObjects(classtype): + return [obj for obj in FreeCAD.ActiveDocument.Objects + if hasattr(obj, "Proxy") + and hasattr(obj.Proxy, "Type") + and obj.Proxy.Type == classtype] def getClosePoints(sh1, angle): '''