From e1e144189204fa9f33ce57b4465eb67829e5558d Mon Sep 17 00:00:00 2001 From: Javi Date: Wed, 16 Jul 2025 08:58:35 +0200 Subject: [PATCH 1/6] =?UTF-8?q?Punto=20de=20restauraci=C3=B3n.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Export/exportDXF.py | 160 ++++++++-- Importer/importOSM.py | 19 +- PVPlantFence.py | 3 +- PVPlantFenceGate.py | 4 +- PVPlantImportGrid.py | 1 + PVPlantPlacement.py | 191 +++--------- PVPlantPlacement.ui | 670 ++++++++++++++++++++-------------------- PVPlantSite.py | 8 +- Project/ProjectSetup.ui | 122 ++++---- 9 files changed, 599 insertions(+), 579 deletions(-) diff --git a/Export/exportDXF.py b/Export/exportDXF.py index 1dbad34..68f4b6f 100644 --- a/Export/exportDXF.py +++ b/Export/exportDXF.py @@ -1,5 +1,6 @@ import math import FreeCAD +import Part from Utils.PVPlantUtils import findObjects if FreeCAD.GuiUp: @@ -259,15 +260,60 @@ class exportDXF: 'rotation': rotation }) - def createPolyline(self, wire): + 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 # ================================================================================= @@ -575,6 +621,58 @@ class LineTypeComboBox(QtWidgets.QComboBox): 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 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''' @@ -610,10 +708,8 @@ class _PVPlantExportDXF(QtGui.QWidget): 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) + for row in layers: + self.add_row(*row) def save_project_settings(self): """Guarda la configuración actual en el proyecto de FreeCAD""" @@ -778,25 +874,25 @@ class _PVPlantExportDXF(QtGui.QWidget): ) def writeArea(self, exporter): - exporter.createPolyline(FreeCAD.ActiveDocument.Site.Boundary, "boundary") - - areas_types = ["Boundaries", "Exclusions", "Offsets"] + 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): - 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"''' + 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"): @@ -811,11 +907,11 @@ class _PVPlantExportDXF(QtGui.QWidget): w.Placement.Base = w.Placement.Base.sub(center) block.add_lwpolyline(getWire(w)) - block.add_circle((0, 0), 0.2, dxfattribs={'color': 2}) + 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": "MyLines"}) - block.add_line((-p, p), (p, -p), dxfattribs={"layer": "MyLines"}) + 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: @@ -868,10 +964,10 @@ class _PVPlantExportDXF(QtGui.QWidget): if FreeCAD.ActiveDocument.Transport: for road in FreeCAD.ActiveDocument.Transport.Group: - base = exporter.createPolyline(road, "External_Roads") + base = exporter.createPolyline(road, "CIVIL External Roads") base.dxf.const_width = road.Width - axis = exporter.createPolyline(road.Base, "External_Roads_Axis") + axis = exporter.createPolyline(road, "CIVIL External Roads Axis") axis.dxf.const_width = .2 def writeTrenches(self, exporter): @@ -976,8 +1072,12 @@ class _PVPlantExportDXF(QtGui.QWidget): self.writeTrenches(exporter) # Crear espacios de papel - self.setup_layout4(exporter.doc) - self.createPaperSpaces(exporter) + #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() diff --git a/Importer/importOSM.py b/Importer/importOSM.py index 024716b..fe5c5a9 100644 --- a/Importer/importOSM.py +++ b/Importer/importOSM.py @@ -42,11 +42,10 @@ class OSMImporter: } self.ssl_context = ssl.create_default_context(cafile=certifi.where()) - def transform_from_latlon(self, lat, lon): - point = ImportElevation.getElevationFromOE([[lat, lon], ]) - return FreeCAD.Vector(point[0].x, point[0].y, point[0].z) * scale - self.Origin - '''x, y, _, _ = utm.from_latlon(lat, lon) - return FreeCAD.Vector(x, y, .0) * scale - self.Origin''' + def transform_from_latlon(self, coordinates): + points = ImportElevation.getElevationFromOE(coordinates) + pts = [FreeCAD.Vector(p.x, p.y, p.z).sub(self.Origin) for p in points] + return pts def get_osm_data(self, bbox): query = f""" @@ -81,11 +80,16 @@ class OSMImporter: root = ET.fromstring(osm_data) # Almacenar nodos transformados + coordinates = [[float(node.attrib['lat']), float(node.attrib['lon'])] for node in root.findall('node')] + coordinates = self.transform_from_latlon(coordinates) + for i, node in enumerate(root.findall('node')): + self. nodes[node.attrib['id']] = coordinates[i] + '''return for node in root.findall('node'): self.nodes[node.attrib['id']] = self.transform_from_latlon( float(node.attrib['lat']), float(node.attrib['lon']) - ) + )''' # Procesar ways for way in root.findall('way'): @@ -162,11 +166,10 @@ class OSMImporter: def create_buildings(self): building_layer = self.create_layer("Buildings") for way_id, data in self.ways_data.items(): + print(data) if 'building' not in data['tags']: continue - print(data) - tags = data['tags'] nodes = [self.nodes[ref] for ref in data['nodes'] if ref in self.nodes] diff --git a/PVPlantFence.py b/PVPlantFence.py index f78ab6e..6cef531 100644 --- a/PVPlantFence.py +++ b/PVPlantFence.py @@ -376,8 +376,7 @@ class _Fence(ArchComponent.Component): site = PVPlantSite.get() if True: # prueba import MeshPart as mp - land = FreeCAD.ActiveDocument.Terrain.Mesh - segments = mp.projectShapeOnMesh(pathwire, land, FreeCAD.Vector(0, 0, 1)) + segments = mp.projectShapeOnMesh(pathwire, site.Terrain.Mesh, FreeCAD.Vector(0, 0, 1)) points=[] for segment in segments: points.extend(segment) diff --git a/PVPlantFenceGate.py b/PVPlantFenceGate.py index ae57455..a468913 100644 --- a/PVPlantFenceGate.py +++ b/PVPlantFenceGate.py @@ -256,7 +256,9 @@ class _CommandPVPlantGate: gate = makePVPlantFence() try: import MeshPart as mp - point1 = mp.projectPointsOnMesh([point1,], FreeCAD.ActiveDocument.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))[0] + import PVPlantSite + site = PVPlantSite.get() + point1 = mp.projectPointsOnMesh([point1,], site.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))[0] except: FreeCAD.Console.PrintError("No se puede encontrar punto 3D.." + "\n") diff --git a/PVPlantImportGrid.py b/PVPlantImportGrid.py index 3d3b4cd..a4ecca5 100644 --- a/PVPlantImportGrid.py +++ b/PVPlantImportGrid.py @@ -122,6 +122,7 @@ def getElevationFromOE(coordinates): try: r = get(query, timeout=20, verify=certifi.where()) # <-- Corrección aquí except RequestException as e: + print(f"Error en la solicitud: {str(e)}") points = [] for i, point in enumerate(coordinates): c = utm.from_latlon(point[0], point[1]) diff --git a/PVPlantPlacement.py b/PVPlantPlacement.py index 94fe41e..667ce1d 100644 --- a/PVPlantPlacement.py +++ b/PVPlantPlacement.py @@ -26,6 +26,7 @@ import Part if FreeCAD.GuiUp: import FreeCADGui, os from PySide import QtCore, QtGui + from PySide.QtGui import QListWidgetItem from PySide.QtCore import QT_TRANSLATE_NOOP else: # \cond @@ -62,7 +63,8 @@ class _PVPlantPlacementTaskPanel: '''The editmode TaskPanel for Schedules''' def __init__(self, obj=None): - self.Terrain = PVPlantSite.get().Terrain + self.site = PVPlantSite.get() + self.Terrain = self.site.Terrain self.FrameSetups = None self.PVArea = None self.Area = None @@ -77,8 +79,10 @@ class _PVPlantPlacementTaskPanel: self.form.setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "way.svg"))) self.form.buttonPVArea.clicked.connect(self.addPVArea) - self.form.buttonAddFrame.clicked.connect(self.addFrame) - self.form.buttonRemoveFrame.clicked.connect(self.removeFrame) + #self.form.buttonAddFrame.clicked.connect(self.addFrames) + #self.form.buttonRemoveFrame.clicked.connect(self.removeFrame) + + self.addFrames() def addPVArea(self): sel = FreeCADGui.Selection.getSelection() @@ -86,21 +90,10 @@ class _PVPlantPlacementTaskPanel: self.PVArea = sel[0] self.form.editPVArea.setText(self.PVArea.Label) - def addFrame(self): - from Mechanical.Frame import PVPlantFrame - selection = FreeCADGui.Selection.getSelection() - self.FrameSetup = selectionFilter(selection, PVPlantFrame.TrackerSetup) - - if len(selection) > 0: - items = [] - for x in range(self.form.listFrameSetups.count()): - items.append(self.form.listFrameSetups.item(x).text()) - if not (selection[0].Name in items): - self.form.listFrameSetups.addItem(selection[0].Name) - - def removeFrame(self): - ''' remove select item from list ''' - self.form.listFrameSetups.takeItem(self.form.listFrameSetups.currentRow()) + def addFrames(self): + for frame_setup in self.site.Frames: + list_item = QListWidgetItem(frame_setup.Name, self.form.listFrameSetups) + list_item.setCheckState(QtCore.Qt.Checked) def createFrameFromPoints(self, dataframe): from Mechanical.Frame import PVPlantFrame @@ -111,6 +104,12 @@ class _PVPlantPlacementTaskPanel: MechanicalGroup.Label = "Frames" FreeCAD.ActiveDocument.MechanicalGroup.addObject(MechanicalGroup) + if self.form.cbSubfolders.checked: + group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", self.PVArea.Label) + group.Label = self.PVArea.Label + MechanicalGroup.addObject(group) + MechanicalGroup = group + placements = dataframe["placement"].tolist() types = dataframe["type"].tolist() frames = [] @@ -121,33 +120,30 @@ class _PVPlantPlacementTaskPanel: newrack.Placement = placements[idx] MechanicalGroup.addObject(newrack) frames.append(newrack) + if self.PVArea.Name.startswith("FrameArea"): self.PVArea.Frames = frames - # TODO: else def getProjected(self, shape): """ returns projected edges from a shape and a direction """ - if shape.BoundBox.ZLength == 0: - edges = shape.Edges - return Part.Face(Part.Wire(edges)) - else: - from Utils import PVPlantUtils as utils - wire = utils.simplifyWire(utils.getProjected(shape)) - if wire.isClosed(): - wire = wire.removeSplitter() - return Part.Face(wire) + return Part.Face(Part.Wire(shape.Edges)) + + from Utils import PVPlantUtils as utils + wire = utils.simplifyWire(utils.getProjected(shape)) + return Part.Face(wire.removeSplitter()) if wire.isClosed() else Part.Face(wire) def calculateWorkingArea(self): self.Area = self.getProjected(self.PVArea.Shape) - tmp = FreeCAD.ActiveDocument.findObjects(Name="ExclusionArea") - if len(tmp): - ProhibitedAreas = list() - for obj in tmp: + exclusion_areas = FreeCAD.ActiveDocument.findObjects(Name="ExclusionArea") + + if exclusion_areas: + prohibited_faces = [] + for obj in exclusion_areas: face = self.getProjected(obj.Base.Shape) if face.isValid(): - ProhibitedAreas.append(face) - self.Area = self.Area.cut(ProhibitedAreas) + prohibited_faces.append(face) + self.Area = self.Area.cut(prohibited_faces) def getAligments(self): # TODO: revisar todo esto: ----------------------------------------------------------------- @@ -280,104 +276,6 @@ class _PVPlantPlacementTaskPanel: placeRegion(df) return df - """def placeonregion_old(frames): # old - for colnum, col in enumerate(frames): - groups = list() - groups.append([col[0]]) - for i in range(1, len(col)): - group = groups[-1] - long = (col[i].sub(group[-1])).Length - long -= width - if long <= dist: - group.append(col[i]) - else: - groups.append([col[i]]) - for group in groups: - points = list() - points.append(group[0].sub(vec1)) - for ind in range(0, len(group) - 1): - points.append((group[ind].sub(vec1) + group[ind + 1].add(vec1)) / 2) - points.append(group[-1].add(vec1)) - points3D = list() - ''' - # v0 - for ind in range(len(points) - 1): - line = Part.LineSegment(points[ind], points[ind + 1]) - tmp = terrain.makeParallelProjection(line.toShape(), FreeCAD.Vector(0, 0, 1)) - if len(tmp.Vertexes) > 0: - if ind == 0: - points3D.append(tmp.Vertexes[0].Point) - points3D.append(tmp.Vertexes[-1].Point) - ''' - # V1 - if type == 0: # Mesh: - import MeshPart as mp - for point in points: - point3D = mp.projectPointsOnMesh([point, ], terrain, FreeCAD.Vector(0, 0, 1)) - if len(point3D) > 0: - points3D.append(point3D[0]) - - else: # Shape: - line = Part.LineSegment(points[0], points[-1]) - tmp = terrain.makeParallelProjection(line.toShape(), FreeCAD.Vector(0, 0, 1)) - if len(tmp.Vertexes) > 0: - tmppoints = [ver.Point for ver in tmp.Vertexes] - if mode == 1: # mode = normal - for point in points: - '''# OPTION 1: - line = Part.Line(point, point + FreeCAD.Vector(0, 0, 10)) - for i in range(len(tmppoints) - 1): - seg = Part.LineSegment(tmppoints[i], tmppoints[i + 1]) - inter = line.intersect(seg) - print(inter) - if len(inter) > 0: - points3D.append(FreeCAD.Vector(inter[0].X, inter[0].Y, inter[0].Z)) - ''' - # OPTION 2: - plane = Part.Plane(point, self.Dir) - for i in range(len(tmppoints) - 1): - seg = Part.LineSegment(tmppoints[i], tmppoints[i + 1]) - inter = plane.intersect(seg) - if len(inter) > 0: - if len(inter[0]): - inter = inter[0] - points3D.append(FreeCAD.Vector(inter[0].X, inter[0].Y, inter[0].Z)) - break - else: # TODO: mode = Trend - # TODO: check: - from scipy import stats - xx = list() - yy = list() - zz = list() - - for pts in tmppoints: - xx.append(pts.x) - yy.append(pts.y) - zz.append(pts.z) - - slope, intercept, r, p, std_err = stats.linregress(yy, zz) - - def myfunc(x): - return slope * x + intercept - - x = list() - x.append(yy[0]) - x.append(yy[-1]) - newzz = list(map(myfunc, x)) - points3D.append(FreeCAD.Vector(xx[0], yy[0], newzz[0])) - points3D.append(FreeCAD.Vector(xx[-1], yy[-1], newzz[1])) - - for ind in range(0, len(points3D) - 1): - pl = FreeCAD.Placement() - vec = points3D[ind] - points3D[ind + 1] - pl.Base = FreeCAD.Vector(group[ind]) - p = (points3D[ind] + points3D[ind + 1]) / 2 - pl.Base.z = p.z - rot = FreeCAD.Rotation(FreeCAD.Vector(-1, 0, 0), vec) - pl.Rotation = FreeCAD.Rotation(rot.toEuler()[0], rot.toEuler()[1], 0) - placements.append(pl) - return placements""" - def isInside(self, frame, point): if self.Area.isInside(point, 10, True): frame.Placement.Base = point @@ -542,23 +440,32 @@ class _PVPlantPlacementTaskPanel: params.SetBool("AutoSaveEnabled", False) FreeCAD.ActiveDocument.RecomputesFrozen = True - items = [] - for x in range(self.form.listFrameSetups.count()): - items.append(FreeCAD.ActiveDocument.getObject(self.form.listFrameSetups.item(x).text())) - tmpframes = list() + items = [ + FreeCAD.ActiveDocument.getObject(item.text()) + for i in range(self.form.listFrameSetups.count()) + if (item := self.form.listFrameSetups.item(i)).checkState() == QtCore.Qt.Checked + ] + + """seen_lengths = set() + tmpframes = [] for frame in sorted(items, key=lambda rack: rack.Length, reverse=True): - found = False + if frame.Length not in seen_lengths: + seen_lengths.add(frame.Length) + tmpframes.append(frame) + '''found = False for tmp in tmpframes: if tmp.Length == frame.Length: found = True break if not found: - tmpframes.append(frame) - self.FrameSetups = tmpframes.copy() - longerFrame = self.FrameSetups[0] + tmpframes.append(frame)''' + self.FrameSetups = tmpframes.copy()""" + + unique_frames = {frame.Length.Value: frame for frame in items} + self.FrameSetups = sorted(list(unique_frames.values()), key=lambda rack: rack.Length, reverse=True) self.gap_col = FreeCAD.Units.Quantity(self.form.editGapCols.text()).Value - self.gap_row = FreeCAD.Units.Quantity(self.form.editGapRows.text()).Value + longerFrame.Length.Value + self.gap_row = FreeCAD.Units.Quantity(self.form.editGapRows.text()).Value + self.FrameSetups[0].Length.Value self.offsetX = FreeCAD.Units.Quantity(self.form.editOffsetHorizontal.text()).Value self.offsetY = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value @@ -581,8 +488,6 @@ class _PVPlantPlacementTaskPanel: print(" -- Tiempo tardado:", total_time) FreeCADGui.Control.closeDialog() FreeCAD.ActiveDocument.recompute() - #return True - # ---------------------------------------------------------------------------------------------------------------------- # function AdjustToTerrain diff --git a/PVPlantPlacement.ui b/PVPlantPlacement.ui index 05e553d..49fc1d4 100644 --- a/PVPlantPlacement.ui +++ b/PVPlantPlacement.ui @@ -15,6 +15,338 @@ Park Settings + + + + Estructura: + + + + + + + + 16777215 + 54 + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Add + + + + + + + Remove + + + + + + + + + + Add + + + + + + + Configuración + + + + 10 + + + 6 + + + + + + De arriba a abajo + + + + + De abajo a arriba + + + + + Del centro a los lados + + + + + + + + + 120 + 16777215 + + + + Dirección Horizontal + + + + + + + + De izquierda a derecha + + + + + De derecha a izquiera + + + + + De centro a los lados + + + + + + + + Alinear estructuras + + + true + + + + + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + QAbstractSpinBox::ButtonSymbols::NoButtons + + + mm + + + 10000 + + + 500 + + + + + + + + 120 + 16777215 + + + + Pitch + + + + + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + QAbstractSpinBox::ButtonSymbols::NoButtons + + + m + + + 3 + + + -10000.000000000000000 + + + 10000.000000000000000 + + + + + + + + 120 + 16777215 + + + + Orientación + + + + + + + + 120 + 16777215 + + + + Offset Horizontal + + + + + + + + 120 + 16777215 + + + + Offset Vertical + + + + + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + QAbstractSpinBox::ButtonSymbols::NoButtons + + + m + + + 3 + + + -10000.000000000000000 + + + 10000.000000000000000 + + + + + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + QAbstractSpinBox::ButtonSymbols::NoButtons + + + m + + + 3 + + + 100.000000000000000 + + + 5.000000000000000 + + + + + + + + Norte - Sur + + + + + Este - Oeste + + + + + + + + + 120 + 16777215 + + + + Dirección Vertical + + + + + + + + 120 + 16777215 + + + + Espacio entre filas + + + + + + + + + + - Inner Spacing + + + + + + @@ -59,10 +391,10 @@ - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - QAbstractSpinBox::NoButtons + QAbstractSpinBox::ButtonSymbols::NoButtons @@ -110,10 +442,10 @@ - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - QAbstractSpinBox::NoButtons + QAbstractSpinBox::ButtonSymbols::NoButtons @@ -135,10 +467,10 @@ - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - QAbstractSpinBox::NoButtons + QAbstractSpinBox::ButtonSymbols::NoButtons 4 @@ -148,10 +480,10 @@ - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - QAbstractSpinBox::NoButtons + QAbstractSpinBox::ButtonSymbols::NoButtons 8 @@ -161,45 +493,6 @@ - - - - Add - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Add - - - - - - - Remove - - - - - - @@ -207,292 +500,9 @@ - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Configuración - - - - 10 - - - 6 - - - - - - 120 - 16777215 - - - - Offset Vertical - - - - - - - - 120 - 16777215 - - - - Orientación - - - - - - - Alinear estructuras - - - true - - - - - - - - 120 - 16777215 - - - - Espacio entre filas - - - - - - - - 120 - 16777215 - - - - Dirección Horizontal - - - - - - - - 120 - 16777215 - - - - Offset Horizontal - - - - - - - - 120 - 16777215 - - - - Pitch - - - - - - - - 120 - 16777215 - - - - Dirección Vertical - - - - - - - - Norte - Sur - - - - - Este - Oeste - - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - QAbstractSpinBox::NoButtons - - - m - - - 3 - - - 100.000000000000000 - - - 5.000000000000000 - - - - - - - - De izquierda a derecha - - - - - De derecha a izquiera - - - - - De centro a los lados - - - - - - - - - De arriba a abajo - - - - - De abajo a arriba - - - - - Del centro a los lados - - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - QAbstractSpinBox::NoButtons - - - m - - - 3 - - - -10000.000000000000000 - - - 10000.000000000000000 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - QAbstractSpinBox::NoButtons - - - m - - - 3 - - - -10000.000000000000000 - - - 10000.000000000000000 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - QAbstractSpinBox::NoButtons - - - mm - - - 10000 - - - 500 - - - - - - - - - - Estructura: - - - - - - - - 16777215 - 54 - - - - diff --git a/PVPlantSite.py b/PVPlantSite.py index dc177b4..242b666 100644 --- a/PVPlantSite.py +++ b/PVPlantSite.py @@ -771,12 +771,12 @@ class _PVPlantSite(ArchSite._Site): import PVPlantImportGrid x, y, zone_number, zone_letter = utm.from_latlon(lat, lon) self.obj.UtmZone = zone_list[zone_number - 1] - zz = PVPlantImportGrid.getElevationFromOE([[lat, lon]]) - self.obj.Origin = FreeCAD.Vector(x * 1000, y * 1000, zz[0].z) - #self.obj.OriginOffset = FreeCAD.Vector(x * 1000, y * 1000, 0) #?? + + point = PVPlantImportGrid.getElevationFromOE([[lat, lon]]) + self.obj.Origin = FreeCAD.Vector(point[0].x, point[0].y, point[0].z) self.obj.Latitude = lat self.obj.Longitude = lon - self.obj.Elevation = zz[0].z + self.obj.Elevation = point[0].z class _ViewProviderSite(ArchSite._ViewProviderSite): diff --git a/Project/ProjectSetup.ui b/Project/ProjectSetup.ui index 80a9b3c..e22643b 100644 --- a/Project/ProjectSetup.ui +++ b/Project/ProjectSetup.ui @@ -39,6 +39,16 @@ 9 + + + + Maximum west-east slope: + + + + + + @@ -46,17 +56,20 @@ - - + + - Frame coloring: + South facing + + + Qt::AlignmentFlag::AlignCenter - Qt::Vertical + Qt::Orientation::Vertical @@ -66,71 +79,20 @@ - - + + - Maximum west-east slope: - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - QAbstractSpinBox::NoButtons - - - º - - - 90.000000000000000 - - - 8.000000000000000 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - QAbstractSpinBox::NoButtons - - - º - - - 90.000000000000000 - - - 2.800000000000000 - - - - - - - - - - South facing - - - Qt::AlignCenter + Frame coloring: - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - QAbstractSpinBox::NoButtons + QAbstractSpinBox::ButtonSymbols::NoButtons º @@ -149,7 +111,45 @@ North Facing - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter + + + + + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + QAbstractSpinBox::ButtonSymbols::NoButtons + + + º + + + 90.000000000000000 + + + 2.800000000000000 + + + + + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + QAbstractSpinBox::ButtonSymbols::NoButtons + + + º + + + 90.000000000000000 + + + 8.000000000000000 -- 2.49.1 From 5db8f5439d6437aa98ded576b2712a633f7e959a Mon Sep 17 00:00:00 2001 From: Javi Date: Thu, 31 Jul 2025 09:58:38 +0200 Subject: [PATCH 2/6] =?UTF-8?q?Punto=20de=20restauraci=C3=B3n.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/PVPlant.iml | 4 + .../Fence/PVPlantFence.py | 291 +++++-------- .../Fence/PVPlantFence.ui | 0 .../Fence/PVPlantFenceGate.py | 2 +- .../Fence/PVPlantFenceGate.ui | 0 .../Fence/PVPlantFencePost.py | 171 +++----- Electrical/PowerConverter/PowerConverter.py | 397 ++++++++++++++++++ Electrical/group.py | 387 +++++++++++++++++ PVPlantPlacement.py | 303 ++++++++----- PVPlantPlacement.ui | 52 ++- PVPlantTools.py | 5 +- Project/Area/PVPlantArea.py | 247 +++++++++-- reload.py | 10 +- requirements.txt | 10 +- 14 files changed, 1382 insertions(+), 497 deletions(-) rename PVPlantFence.py => Civil/Fence/PVPlantFence.py (78%) rename PVPlantFence.ui => Civil/Fence/PVPlantFence.ui (100%) rename PVPlantFenceGate.py => Civil/Fence/PVPlantFenceGate.py (99%) rename PVPlantFenceGate.ui => Civil/Fence/PVPlantFenceGate.ui (100%) rename PVPlantFencePost.py => Civil/Fence/PVPlantFencePost.py (55%) create mode 100644 Electrical/PowerConverter/PowerConverter.py create mode 100644 Electrical/group.py diff --git a/.idea/PVPlant.iml b/.idea/PVPlant.iml index d0876a7..cfa6fbe 100644 --- a/.idea/PVPlant.iml +++ b/.idea/PVPlant.iml @@ -5,4 +5,8 @@ + + \ No newline at end of file diff --git a/PVPlantFence.py b/Civil/Fence/PVPlantFence.py similarity index 78% rename from PVPlantFence.py rename to Civil/Fence/PVPlantFence.py index 6cef531..b031b4e 100644 --- a/PVPlantFence.py +++ b/Civil/Fence/PVPlantFence.py @@ -20,16 +20,18 @@ # * * # *********************************************************************** +import FreeCAD +import Part +import Draft +import MeshPart as mp +import ArchComponent + +import Civil.Fence.PVPlantFencePost as PVPlantFencePost +import PVPlantSite +import Utils.PVPlantUtils as utils import copy import math -import ArchComponent -import Draft -import FreeCAD -import Part - -import PVPlantFencePost -import PVPlantSite if FreeCAD.GuiUp: import FreeCADGui @@ -56,26 +58,28 @@ from PVPlantResources import DirIcons as DirIcons EAST = FreeCAD.Vector(1, 0, 0) -def makeprojection(pathwire): - site = FreeCAD.ActiveDocument.Site - land = site.Terrain.Shape - proj = land.makeParallelProjection(pathwire, FreeCAD.Vector(0, 0, 1)) - return proj - def makePVPlantFence(section, post, path): obj = FreeCAD.ActiveDocument.addObject('Part::FeaturePython', 'Fence') - _Fence(obj) + Fence(obj) obj.Post = post obj.Base = path if FreeCAD.GuiUp: - _ViewProviderFence(obj.ViewObject) + ViewProviderFence(obj.ViewObject) hide(section) hide(post) hide(path) + try: + fende_group = FreeCAD.ActiveDocument.Fences + except: + fende_group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'Fences') + fende_group.Label = "Fences" + FreeCAD.ActiveDocument.CivilGroup.addObject(fende_group) + fende_group.addObject(obj) + FreeCAD.ActiveDocument.recompute() return obj @@ -83,16 +87,8 @@ def hide(obj): if hasattr(obj, 'ViewObject') and obj.ViewObject: obj.ViewObject.Visibility = False -def getAngle(Line1, Line2): - v1 = Line1.Vertexes[1].Point - Line1.Vertexes[0].Point - v2 = Line2.Vertexes[1].Point - Line2.Vertexes[0].Point - return v1.getAngle(v2) - - -def get_parameter_from_v0(edge, offset): - """Return parameter at distance offset from edge.Vertexes[0]. - sb method in Part.TopoShapeEdge??? - """ +def get_parameter_from_v0_old(edge, offset): + """ Return parameter at distance offset from edge.Vertexes[0].sb method in Part.TopoShapeEdge??? """ import DraftVecUtils lpt = edge.valueAt(edge.getParameterByLength(0)) @@ -106,14 +102,16 @@ def get_parameter_from_v0(edge, offset): length = offset return edge.getParameterByLength(length) +def get_parameter_from_v0(edge, offset): + """Parámetro a distancia offset desde el primer vértice""" + lpt = edge.valueAt(edge.getParameterByLength(0)) + vpt = edge.Vertexes[0].Point + + if not vpt.isEqual(lpt, 1e-6): + return edge.getParameterByLength(edge.Length - offset) + return edge.getParameterByLength(offset) def calculatePlacement(globalRotation, edge, offset, RefPt, xlate, align, normal=None): - """Orient shape to tangent at parm offset along edge.""" - import functools - import DraftVecUtils - # http://en.wikipedia.org/wiki/Euler_angles - # start with null Placement point so translate goes to right place. - placement = FreeCAD.Placement() placement.Rotation = globalRotation placement.move(RefPt + xlate) @@ -121,55 +119,23 @@ def calculatePlacement(globalRotation, edge, offset, RefPt, xlate, align, normal if not align: return placement - # unit +Z Probably defined elsewhere? - z = FreeCAD.Vector(0, 0, 1) - # y = FreeCAD.Vector(0, 1, 0) # unit +Y - x = FreeCAD.Vector(1, 0, 0) # unit +X - nullv = FreeCAD.Vector(0, 0, 0) + t = edge.tangentAt(get_parameter_from_v0(edge, offset)).normalize() + n = normal or FreeCAD.Vector(0, 0, 1) + b = t.cross(n).normalize() - # get local coord system - tangent, normal, binormal, if possible - t = edge.tangentAt(get_parameter_from_v0(edge, offset)) - t.normalize() - n = normal - b = t.cross(n) - b.normalize() + # Asegurar sistema de coordenadas derecho + if n.dot(t.cross(b)) < 0: + b = -b - lnodes = z.cross(b) - try: - # Can't normalize null vector. - lnodes.normalize() - except: - # pathological cases: - pass - - print(b, " - ", b.dot(z)) - if abs(b.dot(z)) == 1.0: # 2) binormal is || z - # align shape to tangent only - psi = math.degrees(DraftVecUtils.angle(x, t, z)) - theta = 0.0 - phi = 0.0 - FreeCAD.Console.PrintWarning("Draft PathArray.orientShape - Gimbal lock. Infinite lnodes. Change Path or Base.\n") - else: # regular case - psi = math.degrees(DraftVecUtils.angle(x, lnodes, z)) - theta = math.degrees(DraftVecUtils.angle(z, b, lnodes)) - phi = math.degrees(DraftVecUtils.angle(lnodes, t, b)) - - rotations = [placement.Rotation] - - if psi != 0.0: - rotations.insert(0, FreeCAD.Rotation(z, psi)) - if theta != 0.0: - rotations.insert(0, FreeCAD.Rotation(lnodes, theta)) - if phi != 0.0: - rotations.insert(0, FreeCAD.Rotation(b, phi)) - - if len(rotations) == 1: - finalRotation = rotations[0] - else: - finalRotation = functools.reduce(lambda rot1, rot2: rot1.multiply(rot2), rotations) - - placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), finalRotation.toEuler()[2]) + # Construir matriz + rotation_matrix = FreeCAD.Matrix( + t.x, b.x, n.x, 0, + t.y, b.y, n.y, 0, + t.z, b.z, n.z, 0, + 0, 0, 0, 1 + ) + placement.Rotation = FreeCAD.Rotation(rotation_matrix) return placement def calculatePlacementsOnPath(shapeRotation, pathwire, count, xlate, align): @@ -183,12 +149,8 @@ def calculatePlacementsOnPath(shapeRotation, pathwire, count, xlate, align): import DraftGeomUtils closedpath = DraftGeomUtils.isReallyClosed(pathwire) - normal = DraftGeomUtils.getNormal(pathwire) - if normal: - if normal.z < 0: # asegurarse de que siempre se dibuje por encima del suelo - normal.z *= -1 - else: - normal = FreeCAD.Vector(0, 0, 1) + + normal = FreeCAD.Vector(0, 0, 1) path = Part.__sortEdges__(pathwire.Edges) ends = [] cdist = 0 @@ -241,7 +203,7 @@ def calculatePlacementsOnPath(shapeRotation, pathwire, count, xlate, align): return placements -class _Fence(ArchComponent.Component): +class Fence(ArchComponent.Component): def __init__(self, obj): ArchComponent.Component.__init__(self, obj) self.setProperties(obj) @@ -347,7 +309,6 @@ class _Fence(ArchComponent.Component): QT_TRANSLATE_NOOP("App::Property", "The number of posts used to build the fence")) obj.setEditorMode("Length", 1) - self.Type = "PVPlatFence" def __getstate__(self): @@ -361,74 +322,30 @@ class _Fence(ArchComponent.Component): return None def execute(self, obj): + if not obj.Base or not obj.Post: + return + # 1. Preparar trazado base pathwire = self.calculatePathWire(obj) - if pathwire is None: - # FreeCAD.Console.PrintLog("ArchFence.execute: path " + obj.Base.Name + " has no edges\n") - return - - if not obj.Post: - FreeCAD.Console.PrintLog("ArchFence.execute: Post not set\n") + pathwire = utils.getProjected(pathwire, FreeCAD.Vector(0, 0, 1)) + pathwire = utils.simplifyWire(pathwire) + if not pathwire or not pathwire.Edges: return + # 2. Proyectar sobre terreno (con caché) self.Posts = [] self.Foundations = [] - site = PVPlantSite.get() - if True: # prueba - import MeshPart as mp - segments = mp.projectShapeOnMesh(pathwire, site.Terrain.Mesh, FreeCAD.Vector(0, 0, 1)) - points=[] - for segment in segments: - points.extend(segment) - pathwire = Part.makePolygon(points) - else: - if PVPlantSite.get().Terrain.TypeId == 'Mesh::Feature': - import MeshPart as mp - land = PVPlantSite.get().Terrain.Mesh - pathwire = mp.projectShapeOnMesh(pathwire, land, FreeCAD.Vector(0, 0, 1)) - else: - land = site.Terrain.Shape - pathwire = land.makeParallelProjection(pathwire, FreeCAD.Vector(0, 0, 1)) + site = PVPlantSite.get() + segments = mp.projectShapeOnMesh(pathwire, site.Terrain.Mesh, FreeCAD.Vector(0, 0, 1)) + points=[] + for segment in segments: + points.extend(segment) + pathwire = Part.makePolygon(points) if pathwire is None: return - ''' no sirve: - if len(pathwire.Wires) > 1: - import draftgeoutils - pathwire = draftgeoutils.wires.superWire(pathwire.Edges, True) - Part.show(pathwire) - ''' - - ''' unir todas en una ''' - ''' - if len(pathwire.Wires) > 1: - import Utils.PVPlantUtils as utils - wires = pathwire.Wires - new_wire = [] - to_compare = utils.getPoints(wires.pop(0)) - new_wire.extend(to_compare) - while len(wires)>0: - wire = wires[0] - points = utils.getPoints(wire) - to_remove = None - if points[0] in to_compare: - to_remove = points[0] - if points[-1] in to_compare: - to_remove = points[-1] - if to_remove: - to_compare = points.copy() - points.remove(to_remove) - new_wire.extend(points) - wires.pop() - continue - wires.append(wires.pop()) - pathwire = Part.makePolygon(new_wire) - #Part.show(pathwire) - #return - ''' - sectionLength = obj.Gap.Value postLength = 0 #obj.Post.Diameter.Value #considerarlo 0 porque no influye postPlacements = [] @@ -456,18 +373,19 @@ class _Fence(ArchComponent.Component): postPlacements.extend(placements) + # 5. Generar geometría postShapes, postFoundation = self.calculatePosts(obj, postPlacements) - sections, num = self.calculateSections(obj, postPlacements) + mesh = self.calculate_sections(obj, postPlacements) postShapes = Part.makeCompound(postShapes) postFoundation = Part.makeCompound(postFoundation) - sections = Part.makeCompound(sections) - compound = Part.makeCompound([postShapes, postFoundation, sections]) - obj.Shape = compound - # Give information + # 6. Crear forma final + obj.Shape = Part.makeCompound([postShapes, postFoundation, mesh]) + + # 7. Actualizar propiedades obj.NumberOfSections = count - obj.NumberOfPosts = obj.NumberOfSections + 1 + obj.NumberOfPosts = count + 1 obj.Length = pathLength obj.Concrete = count * postFoundation.SubShapes[0].Volume @@ -496,7 +414,7 @@ class _Fence(ArchComponent.Component): def calculatePostPlacements(self, obj, pathwire, rotation): postWidth = obj.Post.Diameter.Value - transformationVector = FreeCAD.Vector(0, postWidth / 2, 0) + transformationVector = FreeCAD.Vector(0, - postWidth / 2, 0) placements = calculatePlacementsOnPath(rotation, pathwire, int(obj.NumberOfSections) + 1, transformationVector, True) # The placement of the last object is always the second entry in the list. # So we move it to the end: @@ -508,47 +426,36 @@ class _Fence(ArchComponent.Component): posts = [] foundations = [] for placement in postPlacements: - postCopy = obj.Post.Shape.copy() - postCopy = Part.Solid(postCopy) - postCopy.Placement = placement - postCopy.Placement.Base.z += 100 - posts.append(postCopy) + new_post = obj.Post.Shape.copy() + new_post = Part.Solid(new_post) + new_post.Placement = placement + new_post.Placement.Base.z += 100 + posts.append(new_post) foundation = Part.makeCylinder(150, 700) foundation.Placement = placement foundation.Placement.Base.z -= obj.Depth.Value - foundation = foundation.cut(postCopy) + #foundation = foundation.cut(new_post) foundations.append(foundation) return posts, foundations - def calculateSections(self, obj, postPlacements): - shapes = [] - faceNumbers = [] + def calculate_sections(self, obj, postPlacements): + offsetz = FreeCAD.Vector(0, 0, obj.MeshOffsetZ.Value) + meshHeight = FreeCAD.Vector(0, 0, obj.MeshHeight.Value) - offsetz = obj.MeshOffsetZ.Value - meshHeight = obj.MeshHeight.Value + points_down = [] + points_up = [] for i in range(len(postPlacements) - 1): - startPlacement = postPlacements[i] - endPlacement = postPlacements[i + 1] + p1 = postPlacements[i].Base + offsetz + p2 = postPlacements[i + 1].Base + offsetz + p3 = p1 + meshHeight + p4 = p2 + meshHeight + points_down.extend([p1, p2]) + points_up.extend([p3, p4]) - p1 = startPlacement.Base + FreeCAD.Vector(0, 0, offsetz) - p2 = endPlacement.Base + FreeCAD.Vector(0, 0, offsetz) - p3 = p2 + FreeCAD.Vector(0, 0, meshHeight) - p4 = p1 + FreeCAD.Vector(0, 0, meshHeight) - pointlist = [p1, p2, p3, p4, p1] - - try: - pol = Part.makePolygon(pointlist) - face = Part.Face(pol) - shapes.append(face) - faceNumbers.append(1) - except: - print("No es posible crear la cara: ---------------------------------------------------") - print(" +++++ Start: ", startPlacement.Base, " - end: ", endPlacement.Base) - print(" +++++ algo: ", pointlist, "\n") - print("---------------------------------------------------\n") - return (shapes, faceNumbers) + shape = Part.makeRuledSurface(Part.makePolygon(points_down), Part.makePolygon(points_up)) + return shape def calculatePathWire(self, obj): if obj.Base: @@ -561,7 +468,7 @@ class _Fence(ArchComponent.Component): return None -class _ViewProviderFence(ArchComponent.ViewProviderComponent): +class ViewProviderFence(ArchComponent.ViewProviderComponent): "A View Provider for the Fence object" def __init__(self, vobj): @@ -641,7 +548,7 @@ class _ViewProviderFence(ArchComponent.ViewProviderComponent): children.append(self.Object.Gate) return children -class _FenceTaskPanel: +class FenceTaskPanel: '''The TaskPanel to setup the fence''' def __init__(self): @@ -774,15 +681,8 @@ class _FenceTaskPanel: self.form = [self.formFence, self.formPost, self.formFoundation] # valores iniciales y creación del la valla: - import Draft - self.post = PVPlantFencePost.makeFencePost() # Arch.makePipe() + self.post = PVPlantFencePost.makeFencePost() self.post.Label = "Post" - Draft.autogroup(self.post) - - ''' - self.section = self.makeGrid() - self.path = self.section.Base - ''' FreeCAD.ActiveDocument.recompute() self.fence = makePVPlantFence(self.section, self.post, self.path) @@ -858,7 +758,7 @@ class CommandPVPlantFence: return not FreeCAD.ActiveDocument is None def Activated(self): - self.TaskPanel = _FenceTaskPanel() + self.TaskPanel = FenceTaskPanel() FreeCADGui.Control.showDialog(self.TaskPanel) @@ -876,12 +776,13 @@ if FreeCAD.GuiUp: } def IsActive(self): + site = FreeCAD.ActiveDocument.getObject("Site") return (not (FreeCAD.ActiveDocument is None) and - not (FreeCAD.ActiveDocument.getObject("Site") is None) and - not (FreeCAD.ActiveDocument.getObject("Terrain") is None)) + not (site is None) and + not (site.Terrain is None)) - import PVPlantFenceGate + import Civil.Fence.PVPlantFenceGate as PVPlantFenceGate FreeCADGui.addCommand('PVPlantFence', CommandPVPlantFence()) - FreeCADGui.addCommand('PVPlantGate', PVPlantFenceGate._CommandPVPlantGate()) - FreeCADGui.addCommand('PVPlantFencePost', PVPlantFencePost._CommandFencePost()) + FreeCADGui.addCommand('PVPlantGate', PVPlantFenceGate.CommandPVPlantGate()) + FreeCADGui.addCommand('PVPlantFencePost', PVPlantFencePost.CommandFencePost()) #FreeCADGui.addCommand('PVPlantFenceGroup', CommandFenceGroup()) diff --git a/PVPlantFence.ui b/Civil/Fence/PVPlantFence.ui similarity index 100% rename from PVPlantFence.ui rename to Civil/Fence/PVPlantFence.ui diff --git a/PVPlantFenceGate.py b/Civil/Fence/PVPlantFenceGate.py similarity index 99% rename from PVPlantFenceGate.py rename to Civil/Fence/PVPlantFenceGate.py index a468913..d5c6786 100644 --- a/PVPlantFenceGate.py +++ b/Civil/Fence/PVPlantFenceGate.py @@ -202,7 +202,7 @@ class ViewProviderGate: children.append(self.Object.Base) return children -class _CommandPVPlantGate: +class CommandPVPlantGate: "the PVPlant Fence command definition" def __init__(self): diff --git a/PVPlantFenceGate.ui b/Civil/Fence/PVPlantFenceGate.ui similarity index 100% rename from PVPlantFenceGate.ui rename to Civil/Fence/PVPlantFenceGate.ui diff --git a/PVPlantFencePost.py b/Civil/Fence/PVPlantFencePost.py similarity index 55% rename from PVPlantFencePost.py rename to Civil/Fence/PVPlantFencePost.py index 55370e6..e46f0fe 100644 --- a/PVPlantFencePost.py +++ b/Civil/Fence/PVPlantFencePost.py @@ -1,5 +1,6 @@ -import ArchComponent import FreeCAD +import Part +import ArchComponent if FreeCAD.GuiUp: import FreeCADGui @@ -9,8 +10,6 @@ else: # \cond def translate(ctxt, txt): return txt - - def QT_TRANSLATE_NOOP(ctxt, txt): return txt # \endcond @@ -21,20 +20,14 @@ except AttributeError: def _fromUtf8(s): return s - -def makeFencePost(diameter=48, length=3000, placement=None, name="Post"): - "makePipe([baseobj,diamerter,length,placement,name]): creates an pipe object from the given base object" - - if not FreeCAD.ActiveDocument: - FreeCAD.Console.PrintError("No active document. Aborting\n") - return +def makeFencePost(diameter=48, length=3000, placement=None, name="FencePost"): obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", name) obj.Label = name - _FencePost(obj) + FencePost(obj) if FreeCAD.GuiUp: - _ViewProviderFencePost(obj.ViewObject) + ViewProviderFencePost(obj.ViewObject) obj.Length = length obj.Diameter = diameter @@ -45,18 +38,13 @@ def makeFencePost(diameter=48, length=3000, placement=None, name="Post"): def makeFenceReinforcePost(diameter=48, length=3000, placement=None, name="Post"): - "makePipe([baseobj,diamerter,length,placement,name]): creates an pipe object from the given base object" - - if not FreeCAD.ActiveDocument: - FreeCAD.Console.PrintError("No active document. Aborting\n") - return obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", name) obj.Label = name - _FenceReinforcePostPost(obj) + FenceReinforcePost(obj) if FreeCAD.GuiUp: - _ViewProviderFencePost(obj.ViewObject) + ViewProviderFencePost(obj.ViewObject) obj.Length = length obj.Diameter = diameter @@ -66,7 +54,7 @@ def makeFenceReinforcePost(diameter=48, length=3000, placement=None, name="Post" return obj -class _FencePost(ArchComponent.Component): +class FencePost(ArchComponent.Component): def __init__(self, obj): ArchComponent.Component.__init__(self, obj) self.setProperties(obj) @@ -80,10 +68,10 @@ class _FencePost(ArchComponent.Component): obj.addProperty("App::PropertyLength", "Diameter", "Pipe", QT_TRANSLATE_NOOP("App::Property", "The diameter of this pipe, if not based on a profile") ).Diameter = 48 - if not "Thickness" in pl: + '''if not "Thickness" in pl: obj.addProperty("App::PropertyLength", "Thickness", "Pipe", QT_TRANSLATE_NOOP("App::Property", "The Thickness of this pipe, if not based on a profile") - ).Thickness = 4 + ).Thickness = 4''' if not "Length" in pl: obj.addProperty("App::PropertyLength", "Length", "Pipe", QT_TRANSLATE_NOOP("App::Property", "The length of this pipe, if not based on an edge") @@ -94,86 +82,49 @@ class _FencePost(ArchComponent.Component): ArchComponent.Component.onDocumentRestored(self, obj) self.setProperties(obj) + def get_axis(self, obj, lip_heigth): + wire = Part.makeLine(FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(0, 0, obj.Length.Value - lip_heigth)) + #wire = Part.makePolygon(FreeCAD.Vector(0, 0, obj.Length.Value - lip_heigth),) + return Part.Wire(wire) + def execute(self, obj): - import Part pl = obj.Placement - if obj.CloneOf: - obj.Shape = obj.CloneOf.Shape - else: - w = self.getProfile(obj) - try: - # sh = w.makePipeShell([p], True, False, 2) - sh = w.revolve(FreeCAD.Vector(0.0, 0.0, 0.0), FreeCAD.Vector(0.0, 0.0, 1.0), 360) - sh = Part.Solid(sh) - except: - FreeCAD.Console.PrintError("Unable to build the pipe \n") - else: - obj.Shape = sh - obj.Placement = pl + lip_heigth = 20 + radius = obj.Diameter.Value / 2 + # para que sea una función que sirva para los postes rectos y con curva: + axis = self.get_axis(obj, lip_heigth) + profile = Part.Wire([Part.Circle(FreeCAD.Vector(0,0,0), FreeCAD.Vector(0,0,1), radius).toShape()]) + post = axis.makePipeShell([profile, ],True,True,2) + + lip = Part.makeCylinder(radius + 2, lip_heigth) + lip = lip.makeFillet(5, [lip.Edges[0]]) + + # Obtener caras + face_post = post.Faces[2] # Cara superior del cilindro largo + face_lip = lip.Faces[2] # Cara inferior del cilindro corto + + # Calcular centro y normal de las caras + face_post_center = face_post.CenterOfMass + face_post_normal = face_post.normalAt(0, 0) + face_lip_center = face_lip.CenterOfMass + face_lip_normal = face_lip.normalAt(0, 0) + + # Calcular rotación para alinear normales (ajustar dirección) + rotacion = FreeCAD.Rotation(face_lip_normal, -face_post_normal) # Invertir normal del cilindro corto + lip.Placement.Rotation = rotacion.multiply(lip.Placement.Rotation) + + # Calcular traslación: mover centro del cilindro corto al centro del cilindro largo + traslacion = face_post_center - rotacion.multVec(face_lip_center) + lip.Placement.Base = traslacion #face_post_center + + obj.Shape = post.fuse(lip) + obj.Placement = pl return - # ------------------------- Prueba para apoyos de refuerzo: - import math - L = math.pi / 2 * (obj.Diameter.Value - 2 * obj.Thickness.Value) - v1 = FreeCAD.Vector(L / 2, 0, obj.Thickness.Value) - vc1 = FreeCAD.Vector(L / 2 + obj.Thickness.Value, 0, 0) - v2 = FreeCAD.Vector(L / 2, 0, -obj.Thickness.Value) - - v11 = FreeCAD.Vector(-L / 2, 0, obj.Thickness.Value) - vc11 = FreeCAD.Vector(-(L / 2 + obj.Thickness.Value), 0, 0) - v21 = FreeCAD.Vector(-L / 2, 0, -obj.Thickness.Value) - - arc1 = Part.Arc(v1, vc1, v2).toShape() - arc11 = Part.Arc(v11, vc11, v21).toShape() - line1 = Part.LineSegment(v11, v1).toShape() - line2 = Part.LineSegment(v21, v2).toShape() - w = Part.Wire([arc1, line2, arc11, line1]) - face = Part.Face(w) - pro = face.extrude(FreeCAD.Vector(0, 40, 0)) - - #Part.Circle(Center, Normal, Radius) - cir1 = Part.Face(Part.Wire(Part.Circle(FreeCAD.Vector(0, -200, 0), FreeCAD.Vector(0, 1, 0), obj.Diameter.Value / 2).toShape())) - ext = cir1.extrude(FreeCAD.Vector(0, 170, 0)) - cir2 = Part.Circle(FreeCAD.Vector(0, -30, 0), FreeCAD.Vector(0, 1, 0), obj.Diameter.Value/2).toShape() - loft = Part.makeLoft([cir2, w], True) - ext = ext.fuse([loft, pro]) - Part.show(ext) - - - def getProfile(self, obj): - import Part - sin45 = 0.707106781 - radio = obj.Diameter.Value / 2 - taph = 20 - tapw = radio + 2 - chamfer = 5 - chamfer2 = chamfer * sin45 - - edge1 = Part.makeLine(FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(radio, 0, 0)) - edge2 = Part.makeLine(FreeCAD.Vector(radio, 0, 0), FreeCAD.Vector(radio, 0, obj.Length.Value - taph)) - edge3 = Part.makeLine(FreeCAD.Vector(radio, 0, obj.Length.Value - taph), - FreeCAD.Vector(tapw, 0, obj.Length.Value - taph)) - edge4 = Part.makeLine(FreeCAD.Vector(tapw, 0, obj.Length.Value - taph), - FreeCAD.Vector(tapw, 0, obj.Length.Value - chamfer)) - if True: - edge5 = Part.makeLine(FreeCAD.Vector(tapw, 0, obj.Length.Value - chamfer), - FreeCAD.Vector(tapw - chamfer, 0, obj.Length.Value)) - else: - edge5 = Part.Arc(FreeCAD.Vector(tapw, 0, obj.Length.Value - chamfer), - FreeCAD.Vector(tapw - chamfer2, 0, obj.Length.Value - chamfer2), - FreeCAD.Vector(tapw - chamfer, 0, obj.Length.Value) - ).toShape() - edge6 = Part.makeLine(FreeCAD.Vector(tapw - chamfer, 0, obj.Length.Value), - FreeCAD.Vector(0, 0, obj.Length.Value)) - w = Part.Wire([edge1, edge2, edge3, edge4, edge5, edge6]) - - return w - - -class _FenceReinforcePost(ArchComponent.Component): +class FenceReinforcePost(ArchComponent.Component): def __init__(self, obj): ArchComponent.Component.__init__(self, obj) self.setProperties(obj) @@ -199,10 +150,18 @@ class _FenceReinforcePost(ArchComponent.Component): self.setProperties(obj) def execute(self, obj): - pl = obj.Placement - w = self.getWire(obj) + lip_heigth = 20 + post = Part.makeCylinder(obj.Diameter.Value / 2, obj.Length.Value - lip_heigth) + lip = Part.makeCylinder(obj.Diameter.Value / 2 + 2, lip_heigth) + lip = lip.makeFillet(5, [lip.Edges[0]]) + lip.translate(FreeCAD.Vector(0, 0, obj.Length.Value - lip_heigth)) + obj.Shape = post.fuse(lip) + obj.Placement = pl + return + + w = self.getWire(obj) try: # sh = w.makePipeShell([p], True, False, 2) sh = w.revolve(FreeCAD.Vector(0.0, 0.0, 0.0), FreeCAD.Vector(0.0, 0.0, 1.0), 360) @@ -244,7 +203,7 @@ class _FenceReinforcePost(ArchComponent.Component): return w -class _ViewProviderFencePost(ArchComponent.ViewProviderComponent): +class ViewProviderFencePost(ArchComponent.ViewProviderComponent): "A View Provider for the Pipe object" def __init__(self, vobj): @@ -254,7 +213,7 @@ class _ViewProviderFencePost(ArchComponent.ViewProviderComponent): return ":/icons/Arch_Pipe_Tree.svg" -class _CommandFencePost: +class CommandFencePost: "the Arch Pipe command definition" def GetResources(self): @@ -269,17 +228,5 @@ class _CommandFencePost: return not FreeCAD.ActiveDocument is None def Activated(self): - if True: - makeFencePost() - else: - FreeCAD.ActiveDocument.openTransaction(translate("Arch", "Create Pipe")) - FreeCADGui.addModule("Arch") - FreeCADGui.doCommand("obj = Arch.makePipe()") - FreeCADGui.addModule("Draft") - FreeCADGui.doCommand("Draft.autogroup(obj)") - FreeCAD.ActiveDocument.commitTransaction() + makeFencePost() FreeCAD.ActiveDocument.recompute() - - -if FreeCAD.GuiUp: - FreeCADGui.addCommand('FencePost', _CommandFencePost()) diff --git a/Electrical/PowerConverter/PowerConverter.py b/Electrical/PowerConverter/PowerConverter.py new file mode 100644 index 0000000..23cc197 --- /dev/null +++ b/Electrical/PowerConverter/PowerConverter.py @@ -0,0 +1,397 @@ +# /********************************************************************** +# * * +# * Copyright (c) 2021 Javier Braña * +# * * +# * This program is free software; you can redistribute it and/or modify* +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307* +# * USA * +# * * +# *********************************************************************** + +import FreeCAD +import ArchComponent +import os +import zipfile +import re + +if FreeCAD.GuiUp: + import FreeCADGui + from DraftTools import translate +else: + # \cond + def translate(ctxt,txt): + return txt + def QT_TRANSLATE_NOOP(ctxt,txt): + return txt + # \endcond + +import os +from PVPlantResources import DirIcons as DirIcons + +__title__ = "PVPlant Areas" +__author__ = "Javier Braña" +__url__ = "http://www.sogos-solar.com" + +import PVPlantResources +from PVPlantResources import DirIcons as DirIcons +Dir3dObjects = os.path.join(PVPlantResources.DirResources, "3dObjects") + + +def makePCS(): + obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "StringInverter") + PowerConverter(obj) + ViewProviderStringInverter(obj.ViewObject) + + try: + folder = FreeCAD.ActiveDocument.StringInverters + except: + folder = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'StringInverters') + folder.Label = "StringInverters" + folder.addObject(obj) + return obj + + +class PowerConverter(ArchComponent.Component): + def __init__(self, obj): + ''' Initialize the Area object ''' + ArchComponent.Component.__init__(self, obj) + + self.oldMPPTs = 0 + + self.Type = None + self.obj = None + self.setProperties(obj) + + def setProperties(self, obj): + pl = obj.PropertiesList + + if not "File" in pl: + obj.addProperty("App::PropertyFile", + "File", + "Inverter", + "The base file this component is built upon") + + if not ("MPPTs" in pl): + obj.addProperty("App::PropertyQuantity", + "MPPTs", + "Inverter", + "Points that define the area" + ).MPPTs = 0 + + if not ("Generator" in pl): + obj.addProperty("App::PropertyEnumeration", + "Generator", + "Inverter", + "Points that define the area" + ).Generator = ["Generic", "Library"] + obj.Generator = "Generic" + + if not ("Type" in pl): + obj.addProperty("App::PropertyString", + "Type", + "Base", + "Points that define the area" + ).Type = "PowerConverter" + obj.setEditorMode("Type", 1) + + self.Type = obj.Type + obj.Proxy = self + + def onDocumentRestored(self, obj): + """ Method run when the document is restored """ + self.setProperties(obj) + + def onBeforeChange(self, obj, prop): + + if prop == "MPPTs": + self.oldMPPTs = int(obj.MPPTs) + + def onChanged(self, obj, prop): + ''' ''' + + if prop == "Generator": + if obj.Generator == "Generic": + obj.setEditorMode("MPPTs", 0) + else: + obj.setEditorMode("MPPTs", 1) + + if prop == "MPPTs": + ''' ''' + if self.oldMPPTs > obj.MPPTs: + ''' borrar sobrantes ''' + obj.removeProperty() + + elif self.oldMPPTs < obj.MPPTs: + ''' crear los faltantes ''' + for i in range(self.oldMPPTs, int(obj.MPPTs)): + ''' ''' + print(i) + else: + pass + + if (prop == "File") and obj.File: + ''' ''' + + def execute(self, obj): + ''' ''' + # obj.Shape: compound + # |- body: compound + # |-- inverter: solid + # |-- door: solid + # |-- holder: solid + + # |- connectors: compound + # |-- DC: compound + # |--- MPPT 1..x: compound + # |---- positive: compound + # |----- connector 1..y: ?? + # |---- negative 1..y: compound + # |----- connector 1..y: ?? + # |-- AC: compound + # |--- R,S,T,: ?? + # |-- Communication + + pl = obj.Placement + filename = self.getFile(obj) + if filename: + parts = self.getPartsList(obj) + if parts: + zdoc = zipfile.ZipFile(filename) + if zdoc: + f = zdoc.open(parts[list(parts.keys())[-1]][1]) + shapedata = f.read() + f.close() + shapedata = shapedata.decode("utf8") + shape = self.cleanShape(shapedata, obj, parts[list(parts.keys())[-1]][2]) + obj.Shape = shape + if not pl.isIdentity(): + obj.Placement = pl + obj.MPPTs = len(shape.SubShapes[1].SubShapes[0].SubShapes) + + def cleanShape(self, shapedata, obj, materials): + "cleans the imported shape" + + import Part + shape = Part.Shape() + shape.importBrepFromString(shapedata) + '''if obj.FuseArch and materials: + # separate lone edges + shapes = [] + for edge in shape.Edges: + found = False + for solid in shape.Solids: + for soledge in solid.Edges: + if edge.hashCode() == soledge.hashCode(): + found = True + break + if found: + break + if found: + break + else: + shapes.append(edge) + print("solids:",len(shape.Solids),"mattable:",materials) + for key,solindexes in materials.items(): + if key == "Undefined": + # do not join objects with no defined material + for solindex in [int(i) for i in solindexes.split(",")]: + shapes.append(shape.Solids[solindex]) + else: + fusion = None + for solindex in [int(i) for i in solindexes.split(",")]: + if not fusion: + fusion = shape.Solids[solindex] + else: + fusion = fusion.fuse(shape.Solids[solindex]) + if fusion: + shapes.append(fusion) + shape = Part.makeCompound(shapes) + try: + shape = shape.removeSplitter() + except Exception: + print(obj.Label,": error removing splitter")''' + return shape + + def getFile(self, obj, filename=None): + "gets a valid file, if possible" + + if not filename: + filename = obj.File + if not filename: + return None + if not filename.lower().endswith(".fcstd"): + return None + if not os.path.exists(filename): + # search for the file in the current directory if not found + basename = os.path.basename(filename) + currentdir = os.path.dirname(obj.Document.FileName) + altfile = os.path.join(currentdir,basename) + if altfile == obj.Document.FileName: + return None + elif os.path.exists(altfile): + return altfile + else: + # search for subpaths in current folder + altfile = None + subdirs = self.splitall(os.path.dirname(filename)) + for i in range(len(subdirs)): + subpath = [currentdir]+subdirs[-i:]+[basename] + altfile = os.path.join(*subpath) + if os.path.exists(altfile): + return altfile + return None + return filename + + def getPartsList(self, obj, filename=None): + + "returns a list of Part-based objects in a FCStd file" + + parts = {} + materials = {} + filename = self.getFile(obj,filename) + if not filename: + return parts + zdoc = zipfile.ZipFile(filename) + with zdoc.open("Document.xml") as docf: + name = None + label = None + part = None + materials = {} + writemode = False + for line in docf: + line = line.decode("utf8") + if "" in line: + writemode = False + elif "" in line: + if name and label and part: + parts[name] = [label,part,materials] + name = None + label = None + part = None + materials = {} + writemode = False + return parts + + def getColors(self,obj): + + "returns the DiffuseColor of the referenced object" + + filename = self.getFile(obj) + if not filename: + return None + part = obj.Part + if not obj.Part: + return None + zdoc = zipfile.ZipFile(filename) + if not "GuiDocument.xml" in zdoc.namelist(): + return None + colorfile = None + with zdoc.open("GuiDocument.xml") as docf: + writemode1 = False + writemode2 = False + for line in docf: + line = line.decode("utf8") + if (" target_power * 1.05: + continue + + group.append(sorted_trackers[idx]) + total_power += tracker_power + used_indices.add(idx) + + # Buscar vecinos cercanos + neighbors = kdtree.query_ball_point( + sorted_points[idx], + max_distance + ) + + # Filtrar vecinos no usados y ordenar por proximidad al punto inicial + neighbors = [n for n in neighbors if n not in used_indices] + neighbors.sort(key=lambda n: abs(n - start_idx)) + queue.extend(neighbors) + + return group, total_power + + # 7. Barrido principal de oeste a este + for i in range(len(sorted_points)): + if i in used_indices: + continue + + group, total_power = expand_group(i) + + if group: + # Calcular centro del grupo + group_points = np.array([t.Placement.Base[:2] for t in group]) + center = np.mean(group_points, axis=0) + + transformer_groups.append({ + 'trackers': group, + 'total_power': total_power, + 'center': center + }) + + # 8. Manejar grupos residuales (si los hay) + unused_indices = set(range(len(sorted_points))) - used_indices + if unused_indices: + # Intentar añadir trackers residuales a grupos existentes + for idx in unused_indices: + point = sorted_points[idx] + tracker_power = sorted_power[idx] + + # Buscar el grupo más cercano que pueda aceptar este tracker + best_group = None + min_distance = float('inf') + + for group in transformer_groups: + if group['total_power'] + tracker_power <= target_power * 1.05: + dist = np.linalg.norm(point - group['center']) + if dist < min_distance and dist < max_distance * 1.5: + min_distance = dist + best_group = group + + # Añadir al grupo si se encontró uno adecuado + if best_group: + best_group['trackers'].append(sorted_trackers[idx]) + best_group['total_power'] += tracker_power + # Actualizar centro del grupo + group_points = np.array([t.Placement.Base[:2] for t in best_group['trackers']]) + best_group['center'] = np.mean(group_points, axis=0) + else: + # Crear un nuevo grupo con este tracker residual + group = [sorted_trackers[idx]] + center = point + transformer_groups.append({ + 'trackers': group, + 'total_power': tracker_power, + 'center': center + }) + + # 9. Crear los grupos en FreeCAD + transformer_group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "Transformers") + transformer_group.Label = "Centros de Transformación" + + 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) + + # Añadir propiedades personalizadas + ct_sphere.addProperty("App::PropertyLinkList", "Trackers", "CT", + "Lista de trackers asociados a este CT") + ct_sphere.addProperty("App::PropertyFloat", "TotalPower", "CT", + "Potencia total del grupo (W)") + ct_sphere.addProperty("App::PropertyFloat", "NominalPower", "CT", + "Potencia nominal del transformador (W)") + ct_sphere.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 + + # Configurar visualización + # Calcular color basado en utilización (verde < 100%, amarillo < 110%, rojo > 110%) + utilization = ct_sphere.Utilization + if utilization <= 100: + color = (0.0, 1.0, 0.0) # Verde + elif utilization <= 110: + color = (1.0, 1.0, 0.0) # Amarillo + else: + color = (1.0, 0.0, 0.0) # Rojo + + ct_sphere.ViewObject.ShapeColor = color + ct_sphere.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)" + + # Añadir al grupo principal + transformer_group.addObject(ct_sphere) + + FreeCAD.Console.PrintMessage(f"Se crearon {len(transformer_groups)} centros de transformación\n") + onSelectGatePoint() + + +import FreeCAD, FreeCADGui, Part +import numpy as np +from scipy.stats import linregress +from PySide import QtGui + +class InternalPathCreator: + def __init__(self, gate_point, strategy=1, path_width=4000): + self.gate_point = gate_point + self.strategy = strategy + self.path_width = path_width + self.ct_spheres = [] + self.ct_positions = [] + + def get_transformers(self): + transformers_group = FreeCAD.ActiveDocument.getObject("Transformers") + if not transformers_group: + FreeCAD.Console.PrintError("No se encontró el grupo 'Transformers'\n") + return False + + self.ct_spheres = transformers_group.Group + if not self.ct_spheres: + 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: + base = sphere.Placement.Base + self.ct_positions.append(FreeCAD.Vector(base.x, base.y, 0)) + return True + + def create_paths(self): + if not self.get_transformers(): + return [] + + if self.strategy == 1: + return self.create_direct_paths() + elif self.strategy == 2: + return self.create_unified_path() + else: + FreeCAD.Console.PrintError("Estrategia no válida. Use 1 o 2.\n") + return [] + + def create_direct_paths(self): + """Estrategia 1: Caminos independientes desde cada CT hasta la puerta""" + paths = [] + for ct in self.ct_positions: + paths.append([ct, self.gate_point]) + return paths + + def create_unified_path(self): + """Estrategia 2: Único camino que une todos los CTs y la puerta usando regresión lineal""" + if not self.ct_positions: + return [] + + all_points = self.ct_positions + [self.gate_point] + x = [p.x for p in all_points] + y = [p.y for p in all_points] + + # Manejar caso de puntos alineados verticalmente + if np.std(x) < 1e-6: + sorted_points = sorted(all_points, key=lambda p: p.y) + paths = [] + for i in range(len(sorted_points) - 1): + paths.append([sorted_points[i], sorted_points[i + 1]]) + return paths + + # Calcular regresión lineal + slope, intercept, _, _, _ = linregress(x, y) + + # Función para proyectar puntos + def project_point(point): + x0, y0 = point.x, point.y + if abs(slope) > 1e6: + return FreeCAD.Vector(x0, intercept, 0) + x_proj = (x0 + slope * (y0 - intercept)) / (1 + slope ** 2) + y_proj = slope * x_proj + intercept + return FreeCAD.Vector(x_proj, y_proj, 0) + + projected_points = [project_point(p) for p in all_points] + + # Calcular distancias a lo largo de la línea + ref_point = projected_points[0] + direction_vector = FreeCAD.Vector(1, slope).normalize() + distances = [] + for p in projected_points: + vec_to_point = p - ref_point + distance = vec_to_point.dot(direction_vector) + distances.append(distance) + + # Ordenar por distancia + sorted_indices = np.argsort(distances) + sorted_points = [all_points[i] for i in sorted_indices] + + # Crear caminos + paths = [] + for i in range(len(sorted_points) - 1): + paths.append([sorted_points[i], sorted_points[i + 1]]) + return paths + + def create_3d_path(self, path_poly): + """Crea geometría 3D para el camino adaptada a orientación norte-sur""" + segments = [] + for i in range(len(path_poly.Vertexes) - 1): + start = path_poly.Vertexes[i].Point + end = path_poly.Vertexes[i + 1].Point + direction = end - start + + # Determinar orientación predominante + if abs(direction.x) > abs(direction.y): + normal = FreeCAD.Vector(0, 1, 0) # Norte-sur + else: + normal = FreeCAD.Vector(1, 0, 0) # Este-oeste + + offset = normal * self.path_width / 2 + + # Crear puntos para la sección transversal + p1 = start + offset + p2 = start - offset + p3 = end - offset + p4 = end + offset + + # Crear cara + wire = Part.makePolygon([p1, p2, p3, p4, p1]) + face = Part.Face(wire) + segments.append(face) + + if segments: + '''road_shape = segments[0].fuse(segments[1:]) + return road_shape.removeSplitter()''' + return Part.makeCompound(segments) + return Part.Shape() + + def build(self): + paths = self.create_paths() + if not paths: + return + + path_group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "InternalPaths") + path_group.Label = f"Caminos Internos (Estrategia {self.strategy})" + + for i, path in enumerate(paths): + poly = Part.makePolygon(path) + + # Objeto para la línea central + path_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"Path_{i + 1}") + path_obj.Shape = poly + path_obj.ViewObject.LineWidth = 3.0 + path_obj.ViewObject.LineColor = (0.0, 0.0, 1.0) + + # Objeto para la superficie 3D + road_shape = self.create_3d_path(poly) + road_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"Road_{i + 1}") + road_obj.Shape = road_shape + road_obj.ViewObject.ShapeColor = (0.7, 0.7, 0.7) + + path_group.addObject(path_obj) + path_group.addObject(road_obj) + + FreeCAD.Console.PrintMessage(f"Se crearon {len(paths)} segmentos de caminos internos\n") + + +# Función para mostrar el diálogo de estrategia +def show_path_strategy_dialog(gate_point): + dialog = QtGui.QDialog() + dialog.setWindowTitle("Seleccionar Estrategia de Caminos") + layout = QtGui.QVBoxLayout(dialog) + + label = QtGui.QLabel("Seleccione la estrategia para crear los caminos internos:") + layout.addWidget(label) + + rb1 = QtGui.QRadioButton("Estrategia 1: Caminos independientes desde cada CT hasta la puerta") + rb1.setChecked(True) + layout.addWidget(rb1) + + rb2 = QtGui.QRadioButton("Estrategia 2: Único camino que une todos los CTs y la puerta") + layout.addWidget(rb2) + + btn_box = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel) + layout.addWidget(btn_box) + + def on_accept(): + strategy = 1 if rb1.isChecked() else 2 + dialog.accept() + creator = InternalPathCreator(gate_point, strategy) + creator.build() + + btn_box.accepted.connect(on_accept) + btn_box.rejected.connect(dialog.reject) + + dialog.exec_() + + +# Uso: seleccionar un punto para la puerta de entrada +def onSelectGatePoint(): + '''gate = FreeCAD.ActiveDocument.findObjects(Name="FenceGate")[0] + gate_point = gate.Placement.Base + show_path_strategy_dialog(gate_point)''' + + sel = FreeCADGui.Selection.getSelectionEx()[0] + show_path_strategy_dialog(sel.SubObjects[0].CenterOfMass) \ No newline at end of file diff --git a/PVPlantPlacement.py b/PVPlantPlacement.py index 667ce1d..04e1a1f 100644 --- a/PVPlantPlacement.py +++ b/PVPlantPlacement.py @@ -78,11 +78,12 @@ class _PVPlantPlacementTaskPanel: self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantPlacement.ui")) self.form.setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "way.svg"))) - self.form.buttonPVArea.clicked.connect(self.addPVArea) - #self.form.buttonAddFrame.clicked.connect(self.addFrames) - #self.form.buttonRemoveFrame.clicked.connect(self.removeFrame) - self.addFrames() + self.maxWidth = max([frame.Width.Value for frame in self.site.Frames]) + + self.form.buttonPVArea.clicked.connect(self.addPVArea) + self.form.editGapCols.valueChanged.connect(self.update_inner_spacing) + self.update_inner_spacing() def addPVArea(self): sel = FreeCADGui.Selection.getSelection() @@ -95,6 +96,10 @@ class _PVPlantPlacementTaskPanel: list_item = QListWidgetItem(frame_setup.Name, self.form.listFrameSetups) list_item.setCheckState(QtCore.Qt.Checked) + def update_inner_spacing(self): + self.form.editInnerSpacing.setText( + ("{} m".format((self.form.editGapCols.value() - self.maxWidth / 1000)))) + def createFrameFromPoints(self, dataframe): from Mechanical.Frame import PVPlantFrame try: @@ -104,22 +109,37 @@ class _PVPlantPlacementTaskPanel: MechanicalGroup.Label = "Frames" FreeCAD.ActiveDocument.MechanicalGroup.addObject(MechanicalGroup) - if self.form.cbSubfolders.checked: - group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", self.PVArea.Label) - group.Label = self.PVArea.Label - MechanicalGroup.addObject(group) - MechanicalGroup = group - - placements = dataframe["placement"].tolist() - types = dataframe["type"].tolist() - frames = [] - for idx in range(len(placements)): - newrack = PVPlantFrame.makeTracker(setup=types[idx]) - newrack.Label = "Tracker" - newrack.Visibility = False - newrack.Placement = placements[idx] - MechanicalGroup.addObject(newrack) - frames.append(newrack) + 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] + else: + group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", label) + group.Label = label + MechanicalGroup.addObject(group) + MechanicalGroup = group + try: + placements = dataframe["placement"].tolist() + types = dataframe["type"].tolist() + frames = [] + for idx in range(len(placements)): + newrack = PVPlantFrame.makeTracker(setup=types[idx]) + newrack.Label = "Tracker" + newrack.Visibility = False + newrack.Placement = placements[idx] + MechanicalGroup.addObject(newrack) + frames.append(newrack) + except: + placements = dataframe[0] + frames = [] + for idx in placements: + print(idx) + newrack = PVPlantFrame.makeTracker(setup=idx[0]) + newrack.Label = "Tracker" + newrack.Visibility = False + newrack.Placement = idx[1] + MechanicalGroup.addObject(newrack) + frames.append(newrack) if self.PVArea.Name.startswith("FrameArea"): self.PVArea.Frames = frames @@ -179,7 +199,7 @@ class _PVPlantPlacementTaskPanel: return np.arange(startx, self.Area.BoundBox.XMax, self.gap_col, dtype=np.int64), \ np.arange(starty, self.Area.BoundBox.YMin, -self.gap_row, dtype=np.int64) - def adjustToTerrain(self, coordinates): + def adjustToTerrain_old(self, coordinates): mode = 1 terrain = self.Terrain.Mesh @@ -276,6 +296,106 @@ class _PVPlantPlacementTaskPanel: placeRegion(df) return df + def _setup_terrain_interpolator(self): + """Prepara interpolador del terreno para ajuste rápido""" + import numpy as np + from scipy.interpolate import LinearNDInterpolator + + mesh = self.Terrain.Mesh + points = np.array([p.Vector for p in mesh.Points]) + bbox = self.Area.BoundBox + + # Filtrar puntos dentro del área de trabajo + in_bbox = [ + p for p in points + if bbox.XMin <= p[0] <= bbox.XMax and + bbox.YMin <= p[1] <= bbox.YMax + ] + + if not in_bbox: + return None + + coords = np.array(in_bbox) + return LinearNDInterpolator(coords[:, :2], coords[:, 2]) + + def adjustToTerrain(self, coordinates): + from scipy.ndimage import label as sclabel + import pandas as pd + import numpy as np + from scipy import stats + import MeshPart + + # Crear matriz binaria + arr = np.array([[1 if obj != 0 else 0 for obj in col] for col in coordinates]) + labeled_array, num_features = sclabel(arr) + + # Construir DataFrame optimizado + data = [] + terrain_interp = self._setup_terrain_interpolator() + + for label in range(1, num_features + 1): + cols, rows = np.where(labeled_array == label) + for idx, (col, row) in enumerate(zip(cols, rows)): + frame_type, placement = coordinates[col][row] + data.append({ + 'ID': len(data) + 1, + 'region': label, + 'type': frame_type, + 'column': col, + 'row': row, + 'placement': placement + }) + + df = pd.DataFrame(data) + + # Ajustar al terreno + for idx, row in df.iterrows(): + pl = row['placement'] + yl = row['type'].Length.Value / 2 + + # Calcular puntos extremos + top_point = FreeCAD.Vector(pl.x, pl.y + yl, 0) + bot_point = FreeCAD.Vector(pl.x, pl.y - yl, 0) + + # Usar interpolador si está disponible + if terrain_interp: + yy = np.linspace(bot_point.y, top_point.y, 10) + xx = np.full(10, pl.x) + zz = terrain_interp(xx, yy) + + if not np.isnan(zz).all(): + slope, intercept, *_ = stats.linregress(yy, zz) + z_top = slope * top_point.y + intercept + z_bot = slope * bot_point.y + intercept + else: + z_top = z_bot = 0 + else: + # Fallback a proyección directa + line = Part.LineSegment(bot_point, top_point).toShape() + projected = MeshPart.projectShapeOnMesh(line, self.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))[0] + if len(projected) >= 2: + yy = [p.y for p in projected] + zz = [p.z for p in projected] + slope, intercept, *_ = stats.linregress(yy, zz) + z_top = slope * top_point.y + intercept + z_bot = slope * bot_point.y + intercept + else: + z_top = z_bot = 0 + + # Actualizar placement + new_top = FreeCAD.Vector(top_point.x, top_point.y, z_top) + new_bot = FreeCAD.Vector(bot_point.x, bot_point.y, z_bot) + + new_pl = FreeCAD.Placement() + new_pl.Base = (new_top + new_bot) / 2 + new_pl.Rotation = FreeCAD.Rotation( + FreeCAD.Vector(-1, 0, 0), + new_top - new_bot + ) + df.at[idx, 'placement'] = new_pl + + return df + def isInside(self, frame, point): if self.Area.isInside(point, 10, True): frame.Placement.Base = point @@ -349,86 +469,68 @@ class _PVPlantPlacementTaskPanel: if countcols == self.form.editColCount.value(): offsetcols += valcols countcols = 0 - print("/n/n") - print(cols) + return self.adjustToTerrain(cols) def calculateNonAlignedArray(self): - gap_col = FreeCAD.Units.Quantity(self.form.editGapCols.text()).Value - gap_row = FreeCAD.Units.Quantity(self.form.editGapRows.text()).Value + max(self.Rack.Shape.BoundBox.XLength, - self.Rack.Shape.BoundBox.YLength) - offset_x = FreeCAD.Units.Quantity(self.form.editOffsetHorizontal.text()).Value - offset_y = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value + pointsx, pointsy = self.getAligments() - Area = self.calculateWorkingArea() + 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)]) + 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 - rec = Part.makePlane(self.Rack.Shape.BoundBox.YLength, self.Rack.Shape.BoundBox.XLength) - - # TODO: revisar todo esto: ----------------------------------------------------------------- - sel = FreeCADGui.Selection.getSelectionEx()[0] - refh = None - refv = None - - if len(sel.SubObjects) == 0: - refh = refv = Area.Edges[0] - - if len(sel.SubObjects) == 1: - refh = refv = sel.SubObjects[0] - - if len(sel.SubObjects) == 2: - if sel.SubObjects[0].BoundBox.XLength > sel.SubObjects[1].BoundBox.XLength: - refh = sel.SubObjects[0] - else: - refh = sel.SubObjects[1] - - if sel.SubObjects[0].BoundBox.YLength > sel.SubObjects[1].BoundBox.YLength: - refv = sel.SubObjects[0] - else: - refv = sel.SubObjects[1] - - steps = int((refv.BoundBox.XMax - Area.BoundBox.XMin + offset_x) / gap_col) - startx = refv.BoundBox.XMax + offset_x - gap_col * steps - # todo end ---------------------------------------------------------------------------------- - - start = FreeCAD.Vector(startx, 0.0, 0.0) - pointsx = np.arange(start.x, Area.BoundBox.XMax, gap_col) - - if self.form.groupCorridor.isChecked(): - if (self.form.editColCount.value() > 0): - xlen = len(pointsx) - count = self.form.editColCount.value() - val = FreeCAD.Units.Quantity(self.form.editColGap.text()).Value - ( - gap_col - min(self.Rack.Shape.BoundBox.XLength, self.Rack.Shape.BoundBox.YLength)) - while count <= xlen: - for i, point in enumerate(pointsx): - if i >= count: - pointsx[i] += val - count += self.form.editColCount.value() + # variables for corridors: + countcols = 0 + countrows = 0 + offsetcols = 0 # ?? + offsetrows = 0 # ?? + valcols = FreeCAD.Units.Quantity(self.form.editColGap.text()).Value - (self.gap_col - yy) pl = [] for point in pointsx: - p1 = FreeCAD.Vector(point, Area.BoundBox.YMax, 0.0) - p2 = FreeCAD.Vector(point, Area.BoundBox.YMin, 0.0) + p1 = FreeCAD.Vector(point, self.Area.BoundBox.YMax, 0.0) + p2 = FreeCAD.Vector(point, self.Area.BoundBox.YMin, 0.0) line = Part.makePolygon([p1, p2]) - inter = Area.section([line]) + inter = self.Area.section([line]) pts = [ver.Point for ver in inter.Vertexes] # todo: sort points for i in range(0, len(pts), 2): line = Part.LineSegment(pts[i], pts[i + 1]) - if line.length() >= rec.BoundBox.YLength: - y1 = pts[i].y - rec.BoundBox.YLength - cp = rec.copy() - cp.Placement.Base = FreeCAD.Vector(pts[i].x - rec.BoundBox.XLength / 2, y1, 0.0) - inter = cp.cut([Area]) - y1 = min([ver.Point.y for ver in inter.Vertexes]) - pointsy = np.arange(y1, pts[i + 1].y, -gap_row) - for point in pointsy: - cp = rec.copy() - cp.Placement.Base = FreeCAD.Vector(pts[i].x - rec.BoundBox.XLength / 2, point, 0.0) - cut = cp.cut([Area], 0) - if len(cut.Vertexes) == 0: - Part.show(cp) - pl.append(point) + 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) + 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 def accept(self): @@ -446,21 +548,6 @@ class _PVPlantPlacementTaskPanel: if (item := self.form.listFrameSetups.item(i)).checkState() == QtCore.Qt.Checked ] - """seen_lengths = set() - tmpframes = [] - for frame in sorted(items, key=lambda rack: rack.Length, reverse=True): - if frame.Length not in seen_lengths: - seen_lengths.add(frame.Length) - tmpframes.append(frame) - '''found = False - for tmp in tmpframes: - if tmp.Length == frame.Length: - found = True - break - if not found: - tmpframes.append(frame)''' - self.FrameSetups = tmpframes.copy()""" - unique_frames = {frame.Length.Value: frame for frame in items} self.FrameSetups = sorted(list(unique_frames.values()), key=lambda rack: rack.Length, reverse=True) @@ -479,8 +566,14 @@ class _PVPlantPlacementTaskPanel: dataframe = self.calculateNonAlignedArray() # 3. Adjust to terrain: self.createFrameFromPoints(dataframe) - FreeCAD.ActiveDocument.commitTransaction() + import Electrical.group as egroup + import importlib + importlib.reload(egroup) + egroup.groupTrackersToTransformers(5000000, self.gap_row + self.FrameSetups[0].Length.Value) + + + FreeCAD.ActiveDocument.commitTransaction() FreeCAD.ActiveDocument.RecomputesFrozen = False params.SetBool("AutoSaveEnabled", auto_save_enabled) @@ -489,6 +582,8 @@ class _PVPlantPlacementTaskPanel: FreeCADGui.Control.closeDialog() FreeCAD.ActiveDocument.recompute() + + # ---------------------------------------------------------------------------------------------------------------------- # function AdjustToTerrain # Take a group of objects and adjust it to the slope and altitude of the terrain mesh. It detects the terrain mesh diff --git a/PVPlantPlacement.ui b/PVPlantPlacement.ui index 49fc1d4..3e77bfe 100644 --- a/PVPlantPlacement.ui +++ b/PVPlantPlacement.ui @@ -22,16 +22,6 @@ - - - - - 16777215 - 54 - - - - @@ -60,20 +50,6 @@ 0 - - - - Add - - - - - - - Remove - - - @@ -335,7 +311,11 @@ - + + + true + + @@ -503,11 +483,29 @@ + + + + + 16777215 + 54 + + + + + + + + Organizar en subcarpetas + + + true + + + - buttonAddFrame - buttonRemoveFrame editPVArea buttonPVArea comboOrientation diff --git a/PVPlantTools.py b/PVPlantTools.py index 5115555..d46ebe1 100644 --- a/PVPlantTools.py +++ b/PVPlantTools.py @@ -673,8 +673,7 @@ if FreeCAD.GuiUp: FreeCADGui.addCommand('PVPlantTracker', PVPlantFrame.CommandTracker()) FreeCADGui.addCommand('RackType', CommandRackGroup()) - -import PVPlantFence +from Civil.Fence import PVPlantFence FreeCADGui.addCommand('PVPlantFenceGroup', PVPlantFence.CommandFenceGroup()) projectlist = [ # "Reload", @@ -712,4 +711,4 @@ pv_mechanical = [ ] objectlist = ['PVPlantTree', - 'PVPlantFence',] \ No newline at end of file + 'PVPlantFenceGroup',] \ No newline at end of file diff --git a/Project/Area/PVPlantArea.py b/Project/Area/PVPlantArea.py index f541820..67227cc 100644 --- a/Project/Area/PVPlantArea.py +++ b/Project/Area/PVPlantArea.py @@ -69,6 +69,7 @@ class _Area: ''' Initialize the Area object ''' self.Type = None self.obj = None + self.setProperties(obj) def setProperties(self, obj): pl = obj.PropertiesList @@ -101,18 +102,18 @@ class _Area: def __setstate__(self, state): pass + def execute(self, obj): + ''' Execute the area object ''' + pass + class _ViewProviderArea: def __init__(self, vobj): - self.Object = vobj.Object vobj.Proxy = self def attach(self, vobj): - ''' - Create Object visuals in 3D view. - ''' - self.Object = vobj.Object - return + ''' Create Object visuals in 3D view. ''' + self.ViewObject = vobj def getIcon(self): ''' @@ -120,6 +121,7 @@ class _ViewProviderArea: ''' return str(os.path.join(DirIcons, "area.svg")) + ''' def claimChildren(self): """ @@ -159,17 +161,10 @@ class _ViewProviderArea: pass def __getstate__(self): - """ - Save variables to file. - """ return None def __setstate__(self, state): - """ - Get variables from file. - """ - return None - + pass ''' Frame Area ''' @@ -311,17 +306,14 @@ class ViewProviderFrameArea(_ViewProviderArea): ''' offsets ''' - - def makeOffsetArea(base = None, val=None): obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "OffsetArea") OffsetArea(obj) obj.Base = base ViewProviderOffsetArea(obj.ViewObject) if val: - obj.Distance = val + obj.OffsetDistance = val - offsets = None try: offsetsgroup = FreeCAD.ActiveDocument.Offsets except: @@ -334,11 +326,13 @@ def makeOffsetArea(base = None, val=None): class OffsetArea(_Area): def __init__(self, obj): - _Area.__init__(self, obj) - self.setProperties(obj) + '''_Area.__init__(self, obj) + self.setProperties(obj)''' + super().__init__(obj) # Llama al constructor de _Area def setProperties(self, obj): - _Area.setProperties(self, obj) + super().setProperties(obj) # Propiedades de la clase base + pl = obj.PropertiesList if not ("OffsetDistance" in pl): obj.addProperty("App::PropertyDistance", @@ -354,24 +348,28 @@ class OffsetArea(_Area): self.setProperties(obj) def execute(self, obj): - import Utils.PVPlantUtils as utils + # Comprobar dependencias críticas + if not hasattr(obj, "Base") or not obj.Base or not obj.Base.Shape: + return + if not hasattr(PVPlantSite, "get") or not PVPlantSite.get().Terrain: + return base = obj.Base.Shape land = PVPlantSite.get().Terrain.Mesh vec = FreeCAD.Vector(0, 0, 1) + wire = utils.getProjected(base, vec) wire = wire.makeOffset2D(obj.OffsetDistance.Value, 2, False, False, True) - tmp = mp.projectShapeOnMesh(wire, land, vec) + sections = mp.projectShapeOnMesh(wire, land, vec) pts = [] - for section in tmp: + for section in sections: pts.extend(section) - obj.Shape = Part.makePolygon(pts) - def __getstate__(self): - return None - - def __setstate__(self, state): - pass + # Crear forma solo si hay resultados + if sections: + obj.Shape = Part.makePolygon(pts) + else: + obj.Shape = Part.Shape() # Forma vacía si falla class ViewProviderOffsetArea(_ViewProviderArea): @@ -382,14 +380,12 @@ class ViewProviderOffsetArea(_ViewProviderArea): def claimChildren(self): """ Provides object grouping """ children = [] - if self.Object.Base: - children.append(self.Object.Base) + if self.ViewObject and self.ViewObject.Object.Base: + children.append(self.ViewObject.Object.Base) return children ''' Forbidden Area: ''' - - def makeProhibitedArea(base = None): obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "ExclusionArea") ProhibitedArea(obj) @@ -416,33 +412,192 @@ class ProhibitedArea(OffsetArea): self.Type = obj.Type = "ProhibitedArea" obj.Proxy = self - def onDocumentRestored(self, obj): - """Method run when the document is restored.""" - self.setProperties(obj) + '''# Propiedades de color + if not hasattr(obj, "OriginalColor"): + obj.addProperty("App::PropertyColor", + "OriginalColor", + "Display", + "Color for original wire") + obj.OriginalColor = (1.0, 0.0, 0.0) # Rojo - def __getstate__(self): - return None + if not hasattr(obj, "OffsetColor"): + obj.addProperty("App::PropertyColor", + "OffsetColor", + "Display", + "Color for offset wire") + obj.OffsetColor = (1.0, 0.5, 0.0) # Naranja - def __setstate__(self, state): - pass + # Propiedades de grosor + if not hasattr(obj, "OriginalWidth"): + obj.addProperty("App::PropertyFloat", + "OriginalWidth", + "Display", + "Line width for original wire") + obj.OriginalWidth = 4.0 + + if not hasattr(obj, "OffsetWidth"): + obj.addProperty("App::PropertyFloat", + "OffsetWidth", + "Display", + "Line width for offset wire") + obj.OffsetWidth = 4.0''' + + def execute(self, obj): + # Comprobar dependencias + if not hasattr(obj, "Base") or not obj.Base or not obj.Base.Shape: + return + if not hasattr(PVPlantSite, "get") or not PVPlantSite.get().Terrain: + return + + base = obj.Base.Shape + land = PVPlantSite.get().Terrain.Mesh + vec = FreeCAD.Vector(0, 0, 1) + + # 1. Crear wire original + original_wire = utils.getProjected(base, vec) + sections_original = mp.projectShapeOnMesh(original_wire, land, vec) + + # 2. Crear wire offset + offset_wire = original_wire.makeOffset2D(obj.OffsetDistance.Value, 2, False, False, True) + sections_offset = mp.projectShapeOnMesh(offset_wire, land, vec) + + # Crear formas compuestas + def make_polygon(sections): + if not sections: + return Part.Shape() + pts = [] + for section in sections: + pts.extend(section) + return Part.makePolygon(pts) + + compounds = [] + if sections_original: + compounds.append(make_polygon(sections_original)) + if sections_offset: + compounds.append(make_polygon(sections_offset)) + + if compounds: + obj.Shape = Part.makeCompound(compounds) + else: + obj.Shape = Part.Shape() + + # Actualizar colores en la vista + if FreeCAD.GuiUp and obj.ViewObject: + obj.ViewObject.Proxy.updateVisual() class ViewProviderForbiddenArea(_ViewProviderArea): + def __init__(self, vobj): + super().__init__(vobj) + # Valores por defecto + self.original_color = (1.0, 0.0, 0.0) # Rojo + self.offset_color = (1.0, 0.5, 0.0) # Naranja + self.original_width = 4.0 + self.offset_width = 4.0 + self.line_widths = [] # Almacenará los grosores por arista + + vobj.LineColor = (1.0, 0.0, 0.0) + vobj.LineWidth = 4 + vobj.PointColor = (1.0, 0.0, 0.0) + vobj.PointSize = 4 + def getIcon(self): - ''' Return object treeview icon ''' + ''' Return object treeview icon. ''' return str(os.path.join(DirIcons, "area_forbidden.svg")) def claimChildren(self): """ Provides object grouping """ children = [] - if self.Object.Base: - children.append(self.Object.Base) + if self.ViewObject and self.ViewObject.Object.Base: + children.append(self.ViewObject.Object.Base) return children + def attach(self, vobj): + super().attach(vobj) + # Inicializar visualización + self.updateVisual() + + def updateVisual(self): + """Actualiza colores y grosores de línea""" + if not hasattr(self, 'ViewObject') or not self.ViewObject or not self.ViewObject.Object: + return + + obj = self.ViewObject.Object + + # Obtener propiedades de color y grosor + try: + self.original_color = obj.OriginalColor + self.offset_color = obj.OffsetColor + self.original_width = obj.OriginalWidth + self.offset_width = obj.OffsetWidth + except: + pass + + # Actualizar colores si hay forma + if hasattr(obj, 'Shape') and obj.Shape and not obj.Shape.isNull(): + if len(obj.Shape.SubShapes) >= 2: + # Asignar colores + colors = [] + colors.append(self.original_color) # Primer wire (original) + colors.append(self.offset_color) # Segundo wire (offset) + self.ViewObject.DiffuseColor = colors + + # Preparar grosores por arista + #self.prepareLineWidths() + + # Asignar grosores usando LineWidthArray + '''if self.line_widths: + self.ViewObject.LineWidthArray = self.line_widths''' + + # Establecer grosor global como respaldo + #self.ViewObject.LineWidth = max(self.original_width, self.offset_width) + + def prepareLineWidths(self): + """Prepara la lista de grosores para cada arista""" + self.line_widths = [] + obj = self.ViewObject.Object + + if hasattr(obj, 'Shape') and obj.Shape and not obj.Shape.isNull(): + # Contar aristas en cada subforma + for i, subshape in enumerate(obj.Shape.SubShapes): + edge_count = len(subshape.Edges) if hasattr(subshape, 'Edges') else 1 + + # Determinar grosor según tipo de wire + width = self.original_width if i == 0 else self.offset_width + + # Asignar el mismo grosor a todas las aristas de este wire + self.line_widths.extend([width] * edge_count) + + def onChanged(self, vobj, prop): + """Maneja cambios en propiedades de visualización""" + if prop in ["LineColor", "PointColor", "ShapeColor", "LineWidth"]: + self.updateVisual() + + def updateData(self, obj, prop): + """Actualiza cuando cambian los datos del objeto""" + if prop == "Shape": + self.updateVisual() + + '''def __getstate__(self): + return { + "original_color": self.original_color, + "offset_color": self.offset_color, + "original_width": self.original_width, + "offset_width": self.offset_width + } + + def __setstate__(self, state): + if "original_color" in state: + self.original_color = state["original_color"] + if "offset_color" in state: + self.offset_color = state["offset_color"] + if "original_width" in state: + self.original_width = state.get("original_width", 4.0) + if "offset_width" in state: + self.offset_width = state.get("offset_width", 4.0)''' + ''' PV Area: ''' - - def makePVSubplant(): obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "PVSubplant") PVSubplant(obj) diff --git a/reload.py b/reload.py index 030da1f..b2b3752 100644 --- a/reload.py +++ b/reload.py @@ -24,11 +24,15 @@ class _CommandReload: def Activated(self): import PVPlantPlacement, \ PVPlantGeoreferencing, PVPlantImportGrid, PVPlantTerrainAnalisys, \ - PVPlantSite, PVPlantRackChecking, PVPlantFence, PVPlantFencePost, PVPlantFenceGate, \ - PVPlantCreateTerrainMesh, \ + PVPlantSite, PVPlantRackChecking, PVPlantCreateTerrainMesh, \ PVPlantFoundation, PVPlantBuilding, PVPlantEarthWorks, PVPlantPad, \ PVPlantRoad, PVPlantTerrain, PVPlantStringing, PVPlantManhole, \ GraphProfile + + from Civil.Fence import PVPlantFenceGate as PVPlantFenceGate + from Civil.Fence import PVPlantFence as PVPlantFence + from Civil.Fence import PVPlantFencePost as PVPlantFencePost + from Civil import PVPlantTrench from Vegetation import PVPlantTreeGenerator @@ -59,9 +63,11 @@ class _CommandReload: importlib.reload(PVPlantSite) importlib.reload(PVPlantFrame) importlib.reload(PVPlantRackChecking) + importlib.reload(PVPlantFence) importlib.reload(PVPlantFenceGate) importlib.reload(PVPlantFencePost) + importlib.reload(PVPlantFoundation) importlib.reload(PVPlantCreateTerrainMesh) importlib.reload(PVPlantTreeGenerator) diff --git a/requirements.txt b/requirements.txt index 8bb799c..b5ac3db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,16 +9,12 @@ setuptools~=68.2.2 laspy~=2.5.3 geopy~=2.4.1 lxml~=4.9.3 -pip~=23.3.2 -wheel~=0.42.0 -Brotli~=1.1.0 -PySocks~=1.7.1 -typing_extensions~=4.9.0 -docutils~=0.20.1 Pillow~=10.1.0 pyproj~=3.7.1 simplekml~=1.3.6 geojson~=3.1.0 certifi~=2023.11.17 SciPy~=1.11.4 -ezdxf~=1.4.1 \ No newline at end of file +pycollada~=0.7.2 +shapely +rtree \ No newline at end of file -- 2.49.1 From 3a188cc47d42cef62b121e6dcce4e3f0ba24a0f7 Mon Sep 17 00:00:00 2001 From: Javi Date: Sun, 17 Aug 2025 13:33:17 +0400 Subject: [PATCH 3/6] new code --- Export/exportPVSyst.py | 350 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 323 insertions(+), 27 deletions(-) diff --git a/Export/exportPVSyst.py b/Export/exportPVSyst.py index 9368f24..2828185 100644 --- a/Export/exportPVSyst.py +++ b/Export/exportPVSyst.py @@ -29,6 +29,15 @@ import Part import numpy import os +from xml.etree.ElementTree import Element, SubElement +import xml.etree.ElementTree as ElementTree +import datetime +from xml.dom import minidom + +from numpy.matrixlib.defmatrix import matrix + +from Utils import PVPlantUtils as utils + if FreeCAD.GuiUp: import FreeCADGui from PySide import QtCore @@ -63,6 +72,11 @@ def check_collada(): FreeCAD.Console.PrintError(translate("PVPlant", "pycollada no encontrado, soporte Collada desactivado.") + "\n") return COLLADA_AVAILABLE + # Asegurar que el texto es Unicode válido +def safe_text(text): + if isinstance(text, bytes): + return text.decode('utf-8', errors='replace') + return text # from ARCH: def triangulate(shape): @@ -249,7 +263,306 @@ def export(exportList, filename, tessellation=1, colors=None): FreeCAD.Console.PrintMessage(translate("Arch", "file %s successfully created.") % filename) -def exportToPVC(path, exportTerrain = False): +def exportToPVC(path, exportTerrain=False): + filename = f"{path}.pvc" + + # 1. Validación inicial de objetos esenciales + site = None + for obj in FreeCAD.ActiveDocument.Objects: + if obj.Name.startswith("Site") and hasattr(obj, 'Terrain'): + site = obj + break + + if not site: + FreeCAD.Console.PrintError("No se encontró objeto 'Site' válido\n") + return False + + # 2. Configuración de metadatos y autor + generated_on = str(datetime.datetime.now()) + + try: + author = FreeCAD.ActiveDocument.CreatedBy + except (AttributeError, UnicodeEncodeError): + author = "Unknown" + + author = author.replace("<", "").replace(">", "") + ver = FreeCAD.Version() + appli = f"PVPlant for FreeCAD {ver[0]}.{ver[1]} build{ver[2]}" + + # 3. Creación estructura XML base + root = Element('COLLADA') + root.set('xmlns', 'http://www.collada.org/2005/11/COLLADASchema') + root.set('version', '1.4.1') + + # 4. Sección + asset = SubElement(root, 'asset') + contrib = SubElement(asset, 'contributor') + SubElement(contrib, 'author').text = safe_text(author) + SubElement(contrib, 'authoring_tool').text = safe_text(appli) + SubElement(asset, 'created').text = generated_on + SubElement(asset, 'modified').text = generated_on + SubElement(asset, 'title').text = safe_text(FreeCAD.ActiveDocument.Name) + unit = SubElement(asset, 'unit') + unit.set('name', 'meter') + unit.set('meter', '1') + + # 5. Materiales y efectos + materials = ["Frames", "Tree_trunk", "Tree_crown", "Topography_mesh"] + + # Library materials + lib_materials = SubElement(root, 'library_materials') + for i, name in enumerate(materials): + mat = SubElement(lib_materials, 'material') + mat.set('id', f'Material{i}') + mat.set('name', name) + SubElement(mat, 'instance_effect').set('url', f'#Material{i}-fx') + + # Library effects + lib_effects = SubElement(root, 'library_effects') + for i, _ in enumerate(materials): + effect = SubElement(lib_effects, 'effect') + effect.set('id', f'Material{i}-fx') + effect.set('name', f'Material{i}') + profile = SubElement(effect, 'profile_COMMON') + technique = SubElement(profile, 'technique') + technique.set('sid', 'standard') + lambert = SubElement(technique, 'lambert') + + # Componentes del material + color = SubElement(SubElement(lambert, 'emission'), 'color') + color.set('sid', 'emission') + color.text = '0.000000 0.000000 0.000000 1.000000' + color = SubElement(SubElement(lambert, 'ambient'), 'color') + color.set('sid', 'ambient') + color.text = '0.200000 0.200000 0.200000 1.000000' + color = SubElement(SubElement(lambert, 'diffuse'), 'color') + color.set('sid', 'diffuse') + color.text = '0.250000 0.500000 0.000000 1.000000' + transparent = SubElement(lambert, 'transparent') + transparent.set('opaque', 'RGB_ZERO') + color = SubElement(transparent, 'color') + color.set('sid', 'transparent') + color.text = '0.000000 0.000000 0.000000 1.000000' + value = SubElement(SubElement(lambert, 'transparency'), 'float') + value.set('sid', 'transparency') + value.text = '0.000000' + + # 6. Geometrías + lib_geometries = SubElement(root, 'library_geometries') + + # 7. Escena visual + lib_visual = SubElement(root, 'library_visual_scenes') + visual_scene = SubElement(lib_visual, 'visual_scene') + visual_scene.set('id', 'Scene') # cambiar a visual_scene_0 + visual_scene.set('name', 'Scene') # cambiar a Default visual scene + + scene_node = SubElement(visual_scene, 'node') + scene_node.set('id', 'node_0_id') + scene_node.set('name', 'node_0_name') + scene_node.set('sid', 'node_0_sid') + + scene_matrix = SubElement(scene_node, 'matrix') + scene_matrix.set('sid', 'matrix_0') + scene_matrix.text = '1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 -1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000' + + root_node = SubElement(scene_node, 'node') + root_node.set('id', 'node_1_id') + root_node.set('name', 'node_1_name') + root_node.set('sid', 'node_1_sid') + + # 8. Función para procesar geometrías + def create_geometry(name, vindex, findex, material_id, objind=0, frame_data=None, isTracker = False, axis_line=None): + """Crea elementos COLLADA para una geometría""" + # Source (vertices) + source_mesh = SubElement(geom, 'mesh') + source = SubElement(source_mesh, 'source') + source.set('id', f'{name}-mesh_source') + float_array = SubElement(source, 'float_array') + float_array.set('id', f'{name}-float_array') + float_array.set('count', str(len(vindex))) + float_array.text = ' '.join(f'{v:.6f}' for v in vindex) + + technique = SubElement(source, 'technique_common') + accessor = SubElement(technique, 'accessor') + accessor.set('count', str(len(vindex))) + accessor.set('source', f'#{name}-float_array') + accessor.set('stride', '3') + for ax in ['X', 'Y', 'Z']: + param = SubElement(accessor, 'param') + param.set('name', ax) + param.set('type', 'float') + + # Vertices + vertices = SubElement(source_mesh, 'vertices') + vertices.set('id', f'{name}-vertices_source') + vertices = SubElement(vertices, 'input') + vertices.set('semantic', 'POSITION') + vertices.set('source', f'#{name}-mesh_source') + + # Triangles + triangles = SubElement(source_mesh, 'triangles') + triangles.set('count', '0') + triangles.set('material', f'Material{material_id}') + triangles_input = SubElement(triangles, 'input') + triangles_input.set('offset', '0') + triangles_input.set('semantic', 'VERTEX') + triangles_input.set('source', f'#{name}-vertices_source') + + p = SubElement(triangles, 'p') + p.text = ' '.join(map(str, findex)) + + # Parámetros especiales para estructuras + + frame_params = SubElement(source_mesh, 'tracker_parameters') + if frame_data: + for key, val in frame_data.items(): + elem = SubElement(frame_params, key) + elem.text = str(val) + + if isTracker: + axis_parameter = SubElement(frame_params, 'axis_vertices') + if axis_line: + for idx, vert in enumerate(axis_line): + array = SubElement(axis_parameter, 'float_array') + array.set('id', f'{name}-axis_float_array{idx}') + array.set('count', '3') + array.text = ' '.join(f'{v:.6f}' for v in vert) + + # 9. Procesar estructuras (frames/trackers) + center = FreeCAD.Vector() + if site.Terrain: + center = site.Terrain.Mesh.BoundBox.Center + + objind = 0 + for frame_type in site.Frames: + is_tracker = "tracker" in frame_type.Proxy.Type.lower() + + modules = frame_type.Shape.SubShapes[0].SubShapes[0] + pts = [] + for i in range(4): + pts.append(modules.BoundBox.getPoint(i)) + + new_shape = Part.Face(Part.makePolygon(pts)) + mesh = Mesh.Mesh(triangulate(new_shape)) + axis = Part.makeLine(FreeCAD.Vector(modules.BoundBox.XMin, 0, modules.BoundBox.ZMax), + FreeCAD.Vector(modules.BoundBox.XMax, 0, modules.BoundBox.ZMax)) + + for obj in FreeCAD.ActiveDocument.Objects: + if hasattr(obj, "Setup") and obj.Setup == frame_type: + # Procesar geometría + mesh.Placement = obj.getGlobalPlacement() + axis.Placement = obj.getGlobalPlacement() + + # Transformar vértices + vindex = [] + for point in mesh.Points: + adjusted = (point.Vector - center) * scale + vindex.extend([ + -adjusted.x, + adjusted.z, + adjusted.y + ]) + + # Índices de caras + findex = [] + for facet in mesh.Facets: + findex.extend(facet.PointIndices) + + # AXIS + # TODO: revisar si es así: + vaxis = [] + for vert in axis.Vertexes: + adjusted = (vert.Point - center) * scale + vaxis.append([ + -adjusted.x, + adjusted.z, + adjusted.y + ]) + + # Crear geometría COLLADA + geom = SubElement(lib_geometries, 'geometry') + geom.set('id', f'Frame_{objind}') + + # Parámetros específicos de estructura + frame_data = { + 'module_width': obj.Setup.ModuleWidth.Value, + 'module_height': obj.Setup.ModuleHeight.Value, + 'module_x_spacing': obj.Setup.ModuleColGap.Value, + 'module_y_spacing': obj.Setup.ModuleRowGap.Value, + 'module_name': 'Generic' + } + + if is_tracker: + frame_data.update({ + 'tracker_type': 'single_axis_trackers', + 'min_phi': obj.Setup.MinPhi.Value, + 'max_phi': obj.Setup.MaxPhi.Value, + 'min_theta': 0, + 'max_theta': 0 + }) + + create_geometry( + name=f'Frame_{objind}', + vindex=vindex, + findex=findex, + material_id=0, + objind=objind, + frame_data=frame_data, + isTracker = is_tracker, + axis_line=vaxis + ) + + # Instancia en escena + instance = SubElement(root_node, 'instance_geometry') + instance.set('url', f'#Frame_{objind}') + + bind_material = SubElement(instance, 'bind_material') + technique_common = SubElement(bind_material, 'technique_common') + instance_material = SubElement(technique_common, 'instance_material') + instance_material.set('symbol', 'Material0') + instance_material.set('target', '#Material0') + + objind += 1 + + # 10. Procesar terreno si está habilitado + if exportTerrain and site.Terrain: + mesh = site.Terrain.Mesh + vindex = [] + for point in mesh.Points: + point = point.Vector + vindex.extend([ + -point.x * SCALE, + point.z * SCALE, + point.y * SCALE + ]) + + findex = [] + for facet in mesh.Facets: + findex.extend(facet.PointIndices) + + geom = SubElement(lib_geometries, 'geometry') + geom.set('id', 'Terrain') + create_geometry('Terrain', vindex, findex, material_id=3) + + instance = SubElement(root_node, 'instance_geometry') + instance.set('url', '#Terrain') + + # 11. Escena principal + scene = SubElement(root, 'scene') + SubElement(scene, 'instance_visual_scene').set('url', '#Scene') + + # 12. Exportar a archivo + xml_str = minidom.parseString( + ElementTree.tostring(root, encoding='utf-8') + ).toprettyxml(indent=" ") + + with open(filename, 'w', encoding='utf-8') as f: + f.write(xml_str) + + FreeCAD.Console.PrintMessage(f"Archivo PVC generado: {filename}\n") + return True + +def exportToPVC_old(path, exportTerrain = False): filename = f"{path}.pvc" from xml.etree.ElementTree import Element, SubElement @@ -291,17 +604,18 @@ def exportToPVC(path, exportTerrain = False): # xml: 1. Asset: asset = SubElement(root, 'asset') + asset_contributor = SubElement(asset, 'contributor') - asset_contributor_autor = SubElement(asset_contributor, 'autor') - #asset_contributor_autor.text = author + asset_contributor_autor = SubElement(asset_contributor, 'author') + asset_contributor_autor.text = author asset_contributor_authoring_tool = SubElement(asset_contributor, 'authoring_tool') - #asset_contributor_authoring_tool.text = appli + asset_contributor_authoring_tool.text = appli asset_contributor_comments = SubElement(asset_contributor, 'comments') asset_keywords = SubElement(asset, 'keywords') asset_revision = SubElement(asset, 'revision') asset_subject = SubElement(asset, 'subject') asset_tittle = SubElement(asset, 'title') - #asset_tittle.text = FreeCAD.ActiveDocument.Name + asset_tittle.text = FreeCAD.ActiveDocument.Name asset_unit = SubElement(asset, 'unit') asset_unit.set('meter', '0.001') asset_unit.set('name', 'millimeter') @@ -359,7 +673,6 @@ def exportToPVC(path, exportTerrain = False): # xml: 4. library_geometries: library_geometries = SubElement(root, 'library_geometries') def add_geometry(objtype, vindex, findex, objind = 0, centers = None): - isFrame = False if objtype == 0: geometryName = 'Frame' @@ -505,36 +818,20 @@ def exportToPVC(path, exportTerrain = False): end_time.text = '1.000000' # xml: 6. scene: - scene = SubElement(root, 'scene') + '''scene = SubElement(root, 'scene') instance = SubElement(scene, 'instance_visual_scene') - instance.set('url', '#') - - full_list_of_objects = FreeCAD.ActiveDocument.Objects + instance.set('url', '#')''' # CASO 1 - FRAMES: frameType = site.Frames - frame_setup = {"type": [], - "footprint": []} - for obj in frameType: - frame_setup["type"] = obj - frame_setup["footprint"] = "" - objind = 0 # TODO: revisar for typ in frameType: isTracker = "tracker" in typ.Proxy.Type.lower() - isTracker = False - - objectlist = FreeCAD.ActiveDocument.findObjects(Name="Tracker") - tmp = [] - for obj in objectlist: - if obj.Name.startswith("TrackerSetup"): - continue - else: - tmp.append(obj) - objectlist = tmp.copy() + #isTracker = False + objectlist = utils.findObjects("Tracker") for obj in objectlist: if obj.Setup == typ: findex = numpy.array([]) @@ -580,7 +877,6 @@ def exportToPVC(path, exportTerrain = False): v = Topology[0][i] vindex[list(range(i * 3, i * 3 + 3))] = (-(v.x - center.x) * scale, (v.z - center.z) * scale, (v.y - center.y) * scale) - # 2. face indices findex = numpy.empty(len(Topology[1]) * 3, numpy.int64) for i in range(len(Topology[1])): -- 2.49.1 From 049898c93918406090499d3a98d04f6b31557c08 Mon Sep 17 00:00:00 2001 From: Javi Date: Sun, 17 Aug 2025 13:34:09 +0400 Subject: [PATCH 4/6] 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): ''' -- 2.49.1 From d61260fdd3174b84dbf2c4e90b3a9a1a8cc3bb67 Mon Sep 17 00:00:00 2001 From: Javi Date: Thu, 20 Nov 2025 00:57:15 +0100 Subject: [PATCH 5/6] updates --- Electrical/PowerConverter/PowerConverter.py | 367 ++----- Electrical/group.py | 3 +- Export/exportDXF.py | 9 +- Importer/importOSM.py | 2 +- InitGui.py | 3 + Mechanical/Frame/PVPlantFrame.py | 77 +- PVPlantPlacement.py | 1050 ++++++++++++++++++- PVPlantTerrain.py | 214 ++-- Project/Area/PVPlantArea.py | 496 ++++++--- package.xml | 3 +- 10 files changed, 1704 insertions(+), 520 deletions(-) diff --git a/Electrical/PowerConverter/PowerConverter.py b/Electrical/PowerConverter/PowerConverter.py index 23cc197..12e9576 100644 --- a/Electrical/PowerConverter/PowerConverter.py +++ b/Electrical/PowerConverter/PowerConverter.py @@ -22,6 +22,7 @@ import FreeCAD import ArchComponent +import Part import os import zipfile import re @@ -48,17 +49,18 @@ import PVPlantResources from PVPlantResources import DirIcons as DirIcons Dir3dObjects = os.path.join(PVPlantResources.DirResources, "3dObjects") +vector = ["Y", "YN", "Z", "ZN", "D"] def makePCS(): - obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "StringInverter") + obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "PowerConversionSystem") PowerConverter(obj) - ViewProviderStringInverter(obj.ViewObject) + ViewProviderPowerConverter(obj.ViewObject) try: - folder = FreeCAD.ActiveDocument.StringInverters + folder = FreeCAD.ActiveDocument.PowerConversionSystemGroup except: - folder = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'StringInverters') - folder.Label = "StringInverters" + folder = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'PowerConversionSystemGroup') + folder.Label = "PowerConversionSystemGroup" folder.addObject(obj) return obj @@ -67,9 +69,6 @@ class PowerConverter(ArchComponent.Component): def __init__(self, obj): ''' Initialize the Area object ''' ArchComponent.Component.__init__(self, obj) - - self.oldMPPTs = 0 - self.Type = None self.obj = None self.setProperties(obj) @@ -77,36 +76,69 @@ class PowerConverter(ArchComponent.Component): def setProperties(self, obj): pl = obj.PropertiesList - if not "File" in pl: - obj.addProperty("App::PropertyFile", - "File", - "Inverter", - "The base file this component is built upon") - - if not ("MPPTs" in pl): - obj.addProperty("App::PropertyQuantity", - "MPPTs", - "Inverter", - "Points that define the area" - ).MPPTs = 0 - - if not ("Generator" in pl): + # Transformer properties + if not "Technology" in pl: obj.addProperty("App::PropertyEnumeration", - "Generator", - "Inverter", - "Points that define the area" - ).Generator = ["Generic", "Library"] - obj.Generator = "Generic" + "Technology", + "Transformer", + "Number of phases and type of transformer" + ).Technology = ["Single Phase Transformer", "Three Phase Transformer"] - if not ("Type" in pl): - obj.addProperty("App::PropertyString", - "Type", - "Base", - "Points that define the area" - ).Type = "PowerConverter" - obj.setEditorMode("Type", 1) + if not "PowerPrimary" in pl: + obj.addProperty("App::PropertyPower", + "PowerPrimary", + "Transformer", + "The base file this component is built upon").PowerPrimary = 6000000000 - self.Type = obj.Type + if not "PowerSecundary1" in pl: + obj.addProperty("App::PropertyPower", + "PowerSecundary1", + "Transformer", + "The base file this component is built upon").PowerSecundary1 = 3000000000 + + if not "PowerSecundary2" in pl: + obj.addProperty("App::PropertyPower", + "PowerSecundary2", + "Transformer", + "The base file this component is built upon").PowerSecundary2 = 3000000000 + + if not "VoltagePrimary" in pl: + obj.addProperty("App::PropertyElectricPotential", + "VoltagePrimary", + "Transformer", + "The base file this component is built upon").VoltagePrimary = 33000000000 + + if not "VoltageSecundary1" in pl: + obj.addProperty("App::PropertyElectricPotential", + "VoltageSecundary1", + "Transformer", + "The base file this component is built upon").VoltageSecundary1 = 11000000000 + + if not "VoltageSecundary2" in pl: + obj.addProperty("App::PropertyElectricPotential", + "VoltageSecundary2", + "Transformer", + "The base file this component is built upon").VoltageSecundary2 = 11000000000 + + if not "VectorPrimary" in pl: + obj.addProperty("App::PropertyEnumeration", + "VectorPrimary", + "Transformer", + "The base file this component is built upon").VectorPrimary = vector + + if not "VectorSecundary1" in pl: + obj.addProperty("App::PropertyEnumeration", + "VectorSecundary1", + "Transformer", + "The base file this component is built upon").VectorSecundary1 = vector + + if not "VectorSecundary2" in pl: + obj.addProperty("App::PropertyEnumeration", + "VectorSecundary2", + "Transformer", + "The base file this component is built upon").VectorSecundary2 = vector + + self.Type = "PowerConverter" obj.Proxy = self def onDocumentRestored(self, obj): @@ -114,263 +146,34 @@ class PowerConverter(ArchComponent.Component): self.setProperties(obj) def onBeforeChange(self, obj, prop): - - if prop == "MPPTs": - self.oldMPPTs = int(obj.MPPTs) + ''' ''' + # This method is called before a property is changed. + # It can be used to validate the property value or to update other properties. + # If the property is not valid, you can raise an exception. + # If you want to prevent the change, you can return False. + # Otherwise, return True to allow the change. + return True def onChanged(self, obj, prop): ''' ''' - if prop == "Generator": - if obj.Generator == "Generic": - obj.setEditorMode("MPPTs", 0) - else: - obj.setEditorMode("MPPTs", 1) - - if prop == "MPPTs": - ''' ''' - if self.oldMPPTs > obj.MPPTs: - ''' borrar sobrantes ''' - obj.removeProperty() - - elif self.oldMPPTs < obj.MPPTs: - ''' crear los faltantes ''' - for i in range(self.oldMPPTs, int(obj.MPPTs)): - ''' ''' - print(i) - else: - pass - - if (prop == "File") and obj.File: - ''' ''' def execute(self, obj): ''' ''' # obj.Shape: compound # |- body: compound - # |-- inverter: solid - # |-- door: solid - # |-- holder: solid - - # |- connectors: compound - # |-- DC: compound - # |--- MPPT 1..x: compound - # |---- positive: compound - # |----- connector 1..y: ?? - # |---- negative 1..y: compound - # |----- connector 1..y: ?? - # |-- AC: compound - # |--- R,S,T,: ?? - # |-- Communication + # |- transformer: solid + # |- primary switchgear: compound + # |- secundary 1 switchgear: compound + # |- secundary 2 switchgear: compound pl = obj.Placement - filename = self.getFile(obj) - if filename: - parts = self.getPartsList(obj) - if parts: - zdoc = zipfile.ZipFile(filename) - if zdoc: - f = zdoc.open(parts[list(parts.keys())[-1]][1]) - shapedata = f.read() - f.close() - shapedata = shapedata.decode("utf8") - shape = self.cleanShape(shapedata, obj, parts[list(parts.keys())[-1]][2]) - obj.Shape = shape - if not pl.isIdentity(): - obj.Placement = pl - obj.MPPTs = len(shape.SubShapes[1].SubShapes[0].SubShapes) + obj.Shape = Part.makeBox(6058, 2438, 2591) # Placeholder for the shape + obj.Placement = pl - def cleanShape(self, shapedata, obj, materials): - "cleans the imported shape" - import Part - shape = Part.Shape() - shape.importBrepFromString(shapedata) - '''if obj.FuseArch and materials: - # separate lone edges - shapes = [] - for edge in shape.Edges: - found = False - for solid in shape.Solids: - for soledge in solid.Edges: - if edge.hashCode() == soledge.hashCode(): - found = True - break - if found: - break - if found: - break - else: - shapes.append(edge) - print("solids:",len(shape.Solids),"mattable:",materials) - for key,solindexes in materials.items(): - if key == "Undefined": - # do not join objects with no defined material - for solindex in [int(i) for i in solindexes.split(",")]: - shapes.append(shape.Solids[solindex]) - else: - fusion = None - for solindex in [int(i) for i in solindexes.split(",")]: - if not fusion: - fusion = shape.Solids[solindex] - else: - fusion = fusion.fuse(shape.Solids[solindex]) - if fusion: - shapes.append(fusion) - shape = Part.makeCompound(shapes) - try: - shape = shape.removeSplitter() - except Exception: - print(obj.Label,": error removing splitter")''' - return shape - def getFile(self, obj, filename=None): - "gets a valid file, if possible" - - if not filename: - filename = obj.File - if not filename: - return None - if not filename.lower().endswith(".fcstd"): - return None - if not os.path.exists(filename): - # search for the file in the current directory if not found - basename = os.path.basename(filename) - currentdir = os.path.dirname(obj.Document.FileName) - altfile = os.path.join(currentdir,basename) - if altfile == obj.Document.FileName: - return None - elif os.path.exists(altfile): - return altfile - else: - # search for subpaths in current folder - altfile = None - subdirs = self.splitall(os.path.dirname(filename)) - for i in range(len(subdirs)): - subpath = [currentdir]+subdirs[-i:]+[basename] - altfile = os.path.join(*subpath) - if os.path.exists(altfile): - return altfile - return None - return filename - - def getPartsList(self, obj, filename=None): - - "returns a list of Part-based objects in a FCStd file" - - parts = {} - materials = {} - filename = self.getFile(obj,filename) - if not filename: - return parts - zdoc = zipfile.ZipFile(filename) - with zdoc.open("Document.xml") as docf: - name = None - label = None - part = None - materials = {} - writemode = False - for line in docf: - line = line.decode("utf8") - if "" in line: - writemode = False - elif "" in line: - if name and label and part: - parts[name] = [label,part,materials] - name = None - label = None - part = None - materials = {} - writemode = False - return parts - - def getColors(self,obj): - - "returns the DiffuseColor of the referenced object" - - filename = self.getFile(obj) - if not filename: - return None - part = obj.Part - if not obj.Part: - return None - zdoc = zipfile.ZipFile(filename) - if not "GuiDocument.xml" in zdoc.namelist(): - return None - colorfile = None - with zdoc.open("GuiDocument.xml") as docf: - writemode1 = False - writemode2 = False - for line in docf: - line = line.decode("utf8") - if (" 0: + self.PVArea = sel[0] + self.form.editPVArea.setText(self.PVArea.Label) + + def addFrames(self): + for frame_setup in self.site.Frames: + list_item = QListWidgetItem(frame_setup.Name, self.form.listFrameSetups) + list_item.setCheckState(QtCore.Qt.Checked) + + def update_inner_spacing(self): + self.form.editInnerSpacing.setText( + ("{} m".format((self.form.editGapCols.value() - self.maxWidth / 1000)))) + + def createFrameFromPoints(self, dataframe): + from Mechanical.Frame import PVPlantFrame + 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() + frames = [] + for idx in range(len(placements)): + newrack = PVPlantFrame.makeTracker(setup=types[idx]) + newrack.Label = "Tracker" + newrack.Visibility = False + newrack.Placement = placements[idx] + group.addObject(newrack) + frames.append(newrack) + except: + placements = dataframe[0] + frames = [] + for idx in placements: + print(idx) + newrack = PVPlantFrame.makeTracker(setup=idx[0]) + newrack.Label = "Tracker" + newrack.Visibility = False + newrack.Placement = idx[1] + groupq.addObject(newrack) + frames.append(newrack) + + if self.PVArea.Name.startswith("FrameArea"): + self.PVArea.Frames = frames + + def getProjected(self, shape): + """ returns projected edges from a shape and a direction """ + if shape.BoundBox.ZLength == 0: + return Part.Face(Part.Wire(shape.Edges)) + + from Utils import PVPlantUtils as utils + wire = utils.simplifyWire(utils.getProjected(shape)) + return Part.Face(wire.removeSplitter()) if wire.isClosed() else Part.Face(wire) + + def calculateWorkingArea(self): + self.Area = self.getProjected(self.PVArea.Shape) + exclusion_areas = FreeCAD.ActiveDocument.findObjects(Name="ExclusionArea") + + if exclusion_areas: + prohibited_faces = [] + for obj in exclusion_areas: + face = self.getProjected(obj.Shape.SubShapes[1]) + if face.isValid(): + prohibited_faces.append(face) + self.Area = self.Area.cut(prohibited_faces) + + def getAligments(self): + # TODO: revisar todo esto: ----------------------------------------------------------------- + sel = FreeCADGui.Selection.getSelectionEx()[0] + refh = None + refv = None + + if len(sel.SubObjects) == 0: + return + + elif len(sel.SubObjects) == 1: + # Todo: chequear que sea un edge. Si es otra cosa coger el edge[0] de la forma + refh = refv = sel.SubObjects[0] + + elif len(sel.SubObjects) > 1: + # Todo: chequear que sea un edge. Si es otra cosa coger el edge[0] de la forma + if sel.SubObjects[0].BoundBox.XLength > sel.SubObjects[1].BoundBox.XLength: + refh = sel.SubObjects[0] + else: + refh = sel.SubObjects[1] + + if sel.SubObjects[0].BoundBox.YLength > sel.SubObjects[1].BoundBox.YLength: + refv = sel.SubObjects[0] + else: + refv = sel.SubObjects[1] + + steps = int((refv.BoundBox.XMax - self.Area.BoundBox.XMin + self.offsetX) / self.gap_col) + startx = int(refv.BoundBox.XMin + self.offsetX - self.gap_col * steps) + steps = int((refh.BoundBox.YMin - self.Area.BoundBox.YMax + self.offsetY) / self.gap_row) + starty = int(refh.BoundBox.YMin + self.offsetY + self.gap_row * steps) + # todo end ---------------------------------------------------------------------------------- + + return np.arange(startx, self.Area.BoundBox.XMax, self.gap_col, dtype=np.int64), \ + np.arange(starty, self.Area.BoundBox.YMin, -self.gap_row, dtype=np.int64) + + def adjustToTerrain_old(self, coordinates): + mode = 1 + terrain = self.Terrain.Mesh + + def placeRegion(df): # TODO: new + import MeshPart as mp + from scipy import stats + linregression = [] + for colnum in df.column.unique().tolist(): + dftmp = df[df["column"] == colnum] + for id in dftmp["ID"].tolist(): + data = df.loc[df['ID'] == id] + frametype = data["type"].tolist()[0] + # col = data["column"] + # row = data["row"] + base = data["placement"].tolist()[0] + + yl = frametype.Length.Value / 2 + ptop = FreeCAD.Vector(base) + ptop.y += yl + pbot = FreeCAD.Vector(base) + pbot.y -= yl + line = Part.LineSegment(ptop, pbot).toShape() + profilepoints = mp.projectShapeOnMesh(line, terrain, FreeCAD.Vector(0, 0, 1))[0] + '''else: # Shape: sumamente lento por lo que quedaría eliminado si no se encuetra otro modo. + tmp = terrain.makeParallelProjection(line, FreeCAD.Vector(0, 0, 1)) + profilepoints = [ver.Point for ver in tmp.Vertexes]''' + + xx = list() + yy = list() + zz = list() + for pts in profilepoints: + xx.append(pts.x) + yy.append(pts.y) + zz.append(pts.z) + slope, intercept, r, p, std_err = stats.linregress(yy, zz) + + # linregression.append(slope, intercept, r, p, std_err) + def myfunc(x): + return slope * x + intercept + + newzz = list(map(myfunc, [yy[0], yy[-1]])) + points3D = list() + points3D.append(FreeCAD.Vector(xx[0], yy[0], newzz[0])) + points3D.append(FreeCAD.Vector(xx[-1], yy[-1], newzz[1])) + linregression.append(points3D) + + # for ind in range(0, len(points3D) - 1): + pl = FreeCAD.Placement() + pl.Base = (points3D[0] + points3D[1]) / 2 + rot = FreeCAD.Rotation(FreeCAD.Vector(-1, 0, 0), points3D[0] - points3D[1]) + pl.Rotation = FreeCAD.Rotation(rot.toEuler()[0], rot.toEuler()[1], 0) + df.at[id - 1, "placement"] = pl + df["regression"] = linregression + + # 01. Grouping: + from scipy.ndimage import label as sclabel + import pandas as pd + tmp = [] + for c, col in enumerate(coordinates): + tmpcol = [] + for n, obj in enumerate(col): + if obj != 0: + tmpcol.append(1) + else: + tmpcol.append(0) + tmp.append(tmpcol) + + data = {"ID": [], + "region": [], + "type": [], + "column": [], + "row": [], + "placement": []} + + arr = np.array(tmp) + labeled_array, num_features = sclabel(arr) + id = 1 + for label in range(1, num_features + 1): + cols, rows = np.where(labeled_array == label) + unique, counts = np.unique(cols, return_counts=True) + result = np.column_stack((unique, counts)) + cnt = 0 + for val, count in result: + for c in range(count): + data["ID"].append(id) + data["region"].append(label) + data["type"].append(coordinates[val][rows[cnt]][0]) + data["column"].append(val) + data["row"].append(rows[cnt]) + data["placement"].append(coordinates[val][rows[cnt]][1]) + cnt += 1 + id += 1 + df = pd.DataFrame(data) + placeRegion(df) + return df + + def _setup_terrain_interpolator(self): + """Prepara interpolador del terreno para ajuste rápido""" + import numpy as np + from scipy.interpolate import LinearNDInterpolator + + mesh = self.Terrain.Mesh + points = np.array([p.Vector for p in mesh.Points]) + bbox = self.Area.BoundBox + + # Filtrar puntos dentro del área de trabajo + in_bbox = [ + p for p in points + if bbox.XMin <= p[0] <= bbox.XMax and + bbox.YMin <= p[1] <= bbox.YMax + ] + + if not in_bbox: + return None + + coords = np.array(in_bbox) + return LinearNDInterpolator(coords[:, :2], coords[:, 2]) + + def adjustToTerrain(self, coordinates): + from scipy.ndimage import label as sclabel + import pandas as pd + import numpy as np + from scipy import stats + import MeshPart + + # Crear matriz binaria + arr = np.array([[1 if obj != 0 else 0 for obj in col] for col in coordinates]) + labeled_array, num_features = sclabel(arr) + + # Construir DataFrame optimizado + data = [] + terrain_interp = self._setup_terrain_interpolator() + + for label in range(1, num_features + 1): + cols, rows = np.where(labeled_array == label) + for idx, (col, row) in enumerate(zip(cols, rows)): + frame_type, placement = coordinates[col][row] + data.append({ + 'ID': len(data) + 1, + 'region': label, + 'type': frame_type, + 'column': col, + 'row': row, + 'placement': placement + }) + + df = pd.DataFrame(data) + + # Ajustar al terreno + for idx, row in df.iterrows(): + pl = row['placement'] + yl = row['type'].Length.Value / 2 + + # Calcular puntos extremos + top_point = FreeCAD.Vector(pl.x, pl.y + yl, 0) + bot_point = FreeCAD.Vector(pl.x, pl.y - yl, 0) + + # Usar interpolador si está disponible + if terrain_interp: + yy = np.linspace(bot_point.y, top_point.y, 10) + xx = np.full(10, pl.x) + zz = terrain_interp(xx, yy) + + if not np.isnan(zz).all(): + slope, intercept, *_ = stats.linregress(yy, zz) + z_top = slope * top_point.y + intercept + z_bot = slope * bot_point.y + intercept + else: + z_top = z_bot = 0 + else: + # Fallback a proyección directa + line = Part.LineSegment(bot_point, top_point).toShape() + projected = MeshPart.projectShapeOnMesh(line, self.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))[0] + if len(projected) >= 2: + yy = [p.y for p in projected] + zz = [p.z for p in projected] + slope, intercept, *_ = stats.linregress(yy, zz) + z_top = slope * top_point.y + intercept + z_bot = slope * bot_point.y + intercept + else: + z_top = z_bot = 0 + + # Actualizar placement + new_top = FreeCAD.Vector(top_point.x, top_point.y, z_top) + new_bot = FreeCAD.Vector(bot_point.x, bot_point.y, z_bot) + + new_pl = FreeCAD.Placement() + new_pl.Base = (new_top + new_bot) / 2 + new_pl.Rotation = FreeCAD.Rotation( + FreeCAD.Vector(-1, 0, 0), + new_top - new_bot + ) + df.at[idx, 'placement'] = new_pl + + return df + + def isInside(self, frame, point): + if self.Area.isInside(point, 10, True): + frame.Placement.Base = point + cut = frame.cut([self.Area]) + if len(cut.Vertexes) == 0: + return True + return False + + def calculateAlignedArray(self): + import FreeCAD + pointsx, pointsy = self.getAligments() + + 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)]) + 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) + + cols = [] + for x in pointsx: + col = [] + for y in pointsy: + found = False + point = FreeCAD.Vector(x + yy_med + offsetcols, y - xx_med + offsetrows, 0.0) + if self.isInside(ref[1], point): + col.append([ref[0], point]) + found = True + continue + else: + for footprint in footprints: + l = int((ref[0].Length - footprint[0].Length) / 2) + for i in range(2): + point1 = FreeCAD.Vector(point) + point1.y = point1.y + l + if self.isInside(footprint[1], point1): + col.append([footprint[0], point1]) + found = True + break + l = -l + if found: + break + if not found: + col.append(0) + cols.append(col) + + # if len(col) > 0: + # code for vertical corridors: + if self.form.groupCorridor.isChecked(): + if self.form.editColCount.value() > 0: + countcols += 1 + if countcols == self.form.editColCount.value(): + offsetcols += valcols + countcols = 0 + + return self.adjustToTerrain(cols) + + 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: + 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]) + + corridor = self.form.groupCorridor.isChecked() + corridor_offset = 0 + count = 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] + pts = sorted(pts, key=lambda p: p.y, reverse=True) + for i in range(0, len(pts), 2): + 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]) + 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(col) + return self.adjustToTerrain(cols) + + def accept(self): + from datetime import datetime + starttime = datetime.now() + + params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Document") + auto_save_enabled = params.GetBool("AutoSaveEnabled") + params.SetBool("AutoSaveEnabled", False) + FreeCAD.ActiveDocument.RecomputesFrozen = True + + items = [ + FreeCAD.ActiveDocument.getObject(item.text()) + for i in range(self.form.listFrameSetups.count()) + if (item := self.form.listFrameSetups.item(i)).checkState() == QtCore.Qt.Checked + ] + + unique_frames = {frame.Length.Value: frame for frame in items} + self.FrameSetups = sorted(list(unique_frames.values()), key=lambda rack: rack.Length, reverse=True) + + self.gap_col = FreeCAD.Units.Quantity(self.form.editGapCols.text()).Value + self.gap_row = FreeCAD.Units.Quantity(self.form.editGapRows.text()).Value + self.FrameSetups[0].Length.Value + self.offsetX = FreeCAD.Units.Quantity(self.form.editOffsetHorizontal.text()).Value + self.offsetY = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value + + FreeCAD.ActiveDocument.openTransaction("Create Placement") + # 1. Calculate working area: + self.calculateWorkingArea() + # 2. Calculate aligned array: + if self.form.cbAlignFrames.isChecked(): + dataframe = self.calculateAlignedArray() + else: + dataframe = self.calculateNonAlignedArray() + # 3. Adjust to terrain: + self.createFrameFromPoints(dataframe) + + import Electrical.group as egroup + import importlib + importlib.reload(egroup) + egroup.groupTrackersToTransformers(5000000, self.gap_row + self.FrameSetups[0].Length.Value) + + + FreeCAD.ActiveDocument.commitTransaction() + FreeCAD.ActiveDocument.RecomputesFrozen = False + params.SetBool("AutoSaveEnabled", auto_save_enabled) + + total_time = datetime.now() - starttime + print(" -- Tiempo tardado:", total_time) + FreeCADGui.Control.closeDialog() + FreeCAD.ActiveDocument.recompute() + + +import numpy as np +import pandas as pd +from scipy.ndimage import label as sclabel +from scipy import stats +from scipy.interpolate import LinearNDInterpolator +import Part +import FreeCAD +import FreeCADGui +from PySide2 import QtCore, QtGui +from PySide2.QtWidgets import QListWidgetItem +import os +import PVPlantResources + + +class _PVPlantPlacementTaskPanel: + '''The editmode TaskPanel for Schedules''' + + def __init__(self, obj=None): + self.site = PVPlantSite.get() + self.Terrain = self.site.Terrain + self.FrameSetups = None + self.PVArea = None + self.Area = None + self.gap_col = .0 + self.gap_row = .0 + self.offsetX = .0 + self.offsetY = .0 + self.Dir = FreeCAD.Vector(0, -1, 0) + self._terrain_interpolator = None + self._frame_footprints_cache = {} + + # UI setup + self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantPlacement.ui")) + self.form.setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "way.svg"))) + + self.addFrames() + self.maxWidth = max([frame.Width.Value for frame in self.site.Frames]) + + self.form.buttonPVArea.clicked.connect(self.addPVArea) + self.form.editGapCols.valueChanged.connect(self.update_inner_spacing) + self.update_inner_spacing() + + def addPVArea(self): + sel = FreeCADGui.Selection.getSelection() + if sel: + self.PVArea = sel[0] + self.form.editPVArea.setText(self.PVArea.Label) + + def addFrames(self): + for frame_setup in self.site.Frames: + list_item = QListWidgetItem(frame_setup.Name, self.form.listFrameSetups) + list_item.setCheckState(QtCore.Qt.Checked) + + def update_inner_spacing(self): + self.form.editInnerSpacing.setText(f"{self.form.editGapCols.value() - self.maxWidth / 1000} m") + + def _get_or_create_frame_group(self): + """Optimized group creation and management""" + doc = FreeCAD.ActiveDocument + + # Get or create main group + main_group = doc.getObject("Frames") or doc.addObject("App::DocumentObjectGroup", "Frames") + if not main_group.Label == "Frames": + main_group.Label = "Frames" + + # Add to MechanicalGroup if exists + if not hasattr(doc, 'MechanicalGroup') and hasattr(doc, 'getObject') and doc.getObject('MechanicalGroup'): + doc.MechanicalGroup.addObject(main_group) + + # Handle subfolder + if self.form.cbSubfolders.isChecked() and self.PVArea: + subgroup_name = f"Frames-{self.PVArea.Label}" + 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) + return subgroup + + return main_group + + def createFrameFromPoints(self, dataframe): + from Mechanical.Frame import PVPlantFrame + doc = FreeCAD.ActiveDocument + + group = self._get_or_create_frame_group() + + frames = [] + placements_key = "placement" if "placement" in dataframe.columns else 0 + + if placements_key == "placement": + placements = dataframe["placement"].tolist() + types = dataframe["type"].tolist() + + for idx, (placement, frame_type) in enumerate(zip(placements, types)): + newrack = PVPlantFrame.makeTracker(setup=frame_type) + newrack.Label = "Tracker" + newrack.Visibility = False + newrack.Placement = placement + group.addObject(newrack) + frames.append(newrack) + + if self.PVArea and self.PVArea.Name.startswith("FrameArea"): + self.PVArea.Frames = frames + + def getProjected(self, shape): + """Optimized projection calculation""" + if shape.BoundBox.ZLength == 0: + return Part.Face(Part.Wire(shape.Edges)) + + from Utils import PVPlantUtils as utils + wire = utils.simplifyWire(utils.getProjected(shape)) + return Part.Face(wire.removeSplitter()) if wire and wire.isClosed() else Part.Face(wire) + + def calculateWorkingArea(self): + """Optimized working area calculation""" + self.Area = self.getProjected(self.PVArea.Shape) + exclusion_areas = FreeCAD.ActiveDocument.findObjects(Name="ExclusionArea") + + if exclusion_areas: + prohibited_faces = [] + for obj in exclusion_areas: + face = self.getProjected(obj.Shape.SubShapes[1]) + if face and face.isValid(): + prohibited_faces.append(face) + + if prohibited_faces: + self.Area = self.Area.cut(prohibited_faces) + + # Clear terrain interpolator cache when area changes + self._terrain_interpolator = None + + def _setup_terrain_interpolator(self): + """Cached terrain interpolator""" + if self._terrain_interpolator is not None: + return self._terrain_interpolator + + mesh = self.Terrain.Mesh + points = np.array([v.Vector for v in mesh.Points]) + bbox = self.Area.BoundBox + + # Filter points within working area efficiently + mask = ((points[:, 0] >= bbox.XMin) & (points[:, 0] <= bbox.XMax) & + (points[:, 1] >= bbox.YMin) & (points[:, 1] <= bbox.YMax)) + filtered_points = points[mask] + + if len(filtered_points) == 0: + self._terrain_interpolator = None + return None + + try: + self._terrain_interpolator = LinearNDInterpolator( + filtered_points[:, :2], filtered_points[:, 2] + ) + except: + self._terrain_interpolator = None + + return self._terrain_interpolator + + def _get_frame_footprint(self, frame): + """Cached footprint calculation""" + frame_key = (frame.Length.Value, frame.Width.Value) + if frame_key not in self._frame_footprints_cache: + l, w = frame.Length.Value, frame.Width.Value + l_med, w_med = l / 2, w / 2 + + footprint = 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) + ]) + footprint.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0)) + + self._frame_footprints_cache[frame_key] = footprint + + return self._frame_footprints_cache[frame_key] + + def _calculate_terrain_adjustment_batch(self, points_data): + """Process terrain adjustments in batches for better performance""" + terrain_interp = self._setup_terrain_interpolator() + results = [] + + for frame_type, base_point in points_data: + yl = frame_type.Length.Value / 2 + top_point = FreeCAD.Vector(base_point.x, base_point.y + yl, 0) + bot_point = FreeCAD.Vector(base_point.x, base_point.y - yl, 0) + + if terrain_interp: + # Use interpolator for faster elevation calculation + yy = np.linspace(bot_point.y, top_point.y, 6) # Reduced points for speed + xx = np.full_like(yy, base_point.x) + try: + zz = terrain_interp(xx, yy) + if not np.isnan(zz).all(): + slope, intercept, *_ = stats.linregress(yy, zz) + z_top = slope * top_point.y + intercept + z_bot = slope * bot_point.y + intercept + else: + z_top = z_bot = 0 + except: + z_top = z_bot = 0 + else: + # Fallback to direct projection (slower) + line = Part.LineSegment(bot_point, top_point).toShape() + try: + import MeshPart + projected = MeshPart.projectShapeOnMesh(line, self.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))[0] + if len(projected) >= 2: + yy = [p.y for p in projected] + zz = [p.z for p in projected] + slope, intercept, *_ = stats.linregress(yy, zz) + z_top = slope * top_point.y + intercept + z_bot = slope * bot_point.y + intercept + else: + z_top = z_bot = 0 + except: + z_top = z_bot = 0 + + new_top = FreeCAD.Vector(top_point.x, top_point.y, z_top) + new_bot = FreeCAD.Vector(bot_point.x, bot_point.y, z_bot) + + new_pl = FreeCAD.Placement() + new_pl.Base = (new_top + new_bot) / 2 + new_pl.Rotation = FreeCAD.Rotation( + FreeCAD.Vector(-1, 0, 0), new_top - new_bot + ) + + results.append((frame_type, new_pl)) + + return results + + def adjustToTerrain(self, coordinates): + """Unified terrain adjustment function for both aligned and non-aligned arrays""" + # Create binary array efficiently + arr = np.array([[int(obj != 0) for obj in col] for col in coordinates], dtype=np.uint8) + labeled_array, num_features = sclabel(arr) + + # Build DataFrame efficiently + data = [] + for label in range(1, num_features + 1): + cols, rows = np.where(labeled_array == label) + for col, row in zip(cols, rows): + frame_type, placement = coordinates[col][row] + data.append({ + 'ID': len(data) + 1, + 'region': label, + 'type': frame_type, + 'column': col, + 'row': row, + 'placement': placement + }) + + if not data: + return pd.DataFrame(columns=['ID', 'region', 'type', 'column', 'row', 'placement']) + + df = pd.DataFrame(data) + + # Process terrain adjustments in batches + points_data = [(row['type'], row['placement']) for _, row in df.iterrows()] + adjusted_results = self._calculate_terrain_adjustment_batch(points_data) + + # Update placements in DataFrame + for idx, (frame_type, new_placement) in enumerate(adjusted_results): + df.at[idx, 'placement'] = new_placement + + return df + + def isInside(self, frame, point): + """Optimized inside check with early termination""" + if not self.Area.isInside(point, 1e-6, True): # Reduced tolerance for speed + return False + + frame_footprint = self._get_frame_footprint(frame) + frame_footprint.Placement.Base = point + + try: + cut = frame_footprint.cut([self.Area]) + return len(cut.Vertexes) == 0 + except: + return False + + def getAligments(self): + """Optimized alignment calculation""" + sel = FreeCADGui.Selection.getSelectionEx() + if not sel or not sel[0].SubObjects: + return np.array([]), np.array([]) + + sub_objects = sel[0].SubObjects + + if len(sub_objects) == 1: + refh = refv = sub_objects[0] + else: + # Choose references based on bounding box dimensions + refh = max(sub_objects[:2], key=lambda x: x.BoundBox.XLength) + refv = max(sub_objects[:2], key=lambda x: x.BoundBox.YLength) + + # Calculate ranges efficiently + startx = refv.BoundBox.XMin + self.offsetX - self.gap_col * int( + (refv.BoundBox.XMax - self.Area.BoundBox.XMin + self.offsetX) / self.gap_col + ) + + starty = refh.BoundBox.YMin + self.offsetY + self.gap_row * int( + (refh.BoundBox.YMin - self.Area.BoundBox.YMax + self.offsetY) / self.gap_row + ) + + x_range = np.arange(startx, self.Area.BoundBox.XMax, self.gap_col, dtype=np.float64) + y_range = np.arange(starty, self.Area.BoundBox.YMin, -self.gap_row, dtype=np.float64) + + return x_range, y_range + + def calculateAlignedArray(self): + """Optimized aligned array calculation""" + pointsx, pointsy = self.getAligments() + if len(pointsx) == 0 or len(pointsy) == 0: + return pd.DataFrame() + + # Precompute footprints once + footprints = [] + for frame in self.FrameSetups: + footprint = self._get_frame_footprint(frame) + footprints.append((frame, footprint)) + + ref_frame, ref_footprint = footprints[0] + ref_length = ref_frame.Length.Value + ref_width = ref_frame.Width.Value + + # Corridor variables + countcols = 0 + valcols = FreeCAD.Units.Quantity(self.form.editColGap.text()).Value - (self.gap_col - ref_width) + corridor_enabled = self.form.groupCorridor.isChecked() + + cols = [] + for x in pointsx: + col = [] + corridor_offset = 0 + + for y in pointsy: + point = FreeCAD.Vector(x + ref_width / 2 + corridor_offset, y - ref_length / 2, 0.0) + found = False + + # Check reference frame first (most common case) + if self.isInside(ref_frame, point): + col.append([ref_frame, point]) + found = True + else: + # Check alternative frames + for frame, footprint in footprints[1:]: + length_diff = (ref_frame.Length.Value - frame.Length.Value) / 2 + for offset in [length_diff, -length_diff]: + test_point = FreeCAD.Vector(point.x, point.y + offset, 0.0) + if self.isInside(frame, test_point): + col.append([frame, test_point]) + found = True + break + if found: + break + + if not found: + col.append(0) + + # Handle corridors + if corridor_enabled and col: + countcols += 1 + if countcols >= self.form.editColCount.value(): + corridor_offset += valcols + countcols = 0 + + cols.append(col) + + return self.adjustToTerrain(cols) + + def calculateNonAlignedArray(self): + """Optimized non-aligned array calculation""" + pointsx, pointsy = self.getAligments() + if len(pointsx) == 0: + FreeCAD.Console.PrintWarning("No X alignments found.\n") + return pd.DataFrame() + + # Precompute footprints + footprints = [] + for frame in self.FrameSetups: + footprint = self._get_frame_footprint(frame) + footprints.append((frame, footprint)) + + corridor_enabled = self.form.groupCorridor.isChecked() + corridor_count = 0 + corridor_offset = 0 + + cols = [] + for x in pointsx: + col = [] + current_x = x + corridor_offset + + # Create vertical line for intersection + p1 = FreeCAD.Vector(current_x, self.Area.BoundBox.YMax, 0.0) + p2 = FreeCAD.Vector(current_x, self.Area.BoundBox.YMin, 0.0) + line = Part.LineSegment(p1, p2).toShape() + + # Get intersections with area + try: + inter = self.Area.section(line) + pts = sorted([v.Point for v in inter.Vertexes], key=lambda p: p.y, reverse=True) + + for i in range(0, len(pts) - 1, 2): + top, bottom = pts[i], pts[i + 1] + available_height = top.y - bottom.y + + if available_height > footprints[-1][0].Width.Value: + # Use optimized placement algorithm + self._place_frames_in_segment(col, footprints, current_x, top, bottom) + + except Exception as e: + FreeCAD.Console.PrintWarning(f"Error in segment processing: {e}\n") + + # Handle corridor offset + if corridor_enabled and col: + corridor_count += 1 + if corridor_count >= self.form.editColCount.value(): + corridor_offset += 12000 # 12m corridor + corridor_count = 0 + + cols.append(col) + + return self.adjustToTerrain(cols) + + def _place_frames_in_segment(self, col, footprints, x, top, bottom): + """Optimized frame placement within a segment""" + current_y = top.y + frame_heights = [ftp[0].Width.Value for ftp in footprints] + min_frame_height = min(frame_heights) + + while current_y - bottom.y > min_frame_height: + placed = False + + for frame, footprint in footprints: + test_y = current_y - frame.Width.Value / 2 + test_point = FreeCAD.Vector(x, test_y, 0.0) + + if self.isInside(frame, test_point): + col.append([frame, test_point]) + current_y -= frame.Width.Value + placed = True + break + + if not placed: + break + + def accept(self): + from datetime import datetime + starttime = datetime.now() + + # Document optimization + params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Document") + auto_save_enabled = params.GetBool("AutoSaveEnabled") + params.SetBool("AutoSaveEnabled", False) + FreeCAD.ActiveDocument.RecomputesFrozen = True + + try: + # Get selected frames + items = [ + FreeCAD.ActiveDocument.getObject(self.form.listFrameSetups.item(i).text()) + for i in range(self.form.listFrameSetups.count()) + if self.form.listFrameSetups.item(i).checkState() == QtCore.Qt.Checked + ] + + # Remove duplicates efficiently + self.FrameSetups = list({frame.Length.Value: frame for frame in items}.values()) + self.FrameSetups.sort(key=lambda x: x.Length.Value, reverse=True) + + # Parse parameters + self.gap_col = FreeCAD.Units.Quantity(self.form.editGapCols.text()).Value + self.gap_row = FreeCAD.Units.Quantity(self.form.editGapRows.text()).Value + self.FrameSetups[0].Length.Value + self.offsetX = FreeCAD.Units.Quantity(self.form.editOffsetHorizontal.text()).Value + self.offsetY = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value + + FreeCAD.ActiveDocument.openTransaction("Create Placement") + + # Main processing + self.calculateWorkingArea() + + if self.form.cbAlignFrames.isChecked(): + dataframe = self.calculateAlignedArray() + else: + dataframe = self.calculateNonAlignedArray() + + if not dataframe.empty: + self.createFrameFromPoints(dataframe) + + # Group trackers + import Electrical.group as egroup + import importlib + importlib.reload(egroup) + egroup.groupTrackersToTransformers(5000000, self.gap_row) + + FreeCAD.ActiveDocument.commitTransaction() + + finally: + # Restore document settings + FreeCAD.ActiveDocument.RecomputesFrozen = False + params.SetBool("AutoSaveEnabled", auto_save_enabled) + + total_time = datetime.now() - starttime + print(f" -- Total time: {total_time}") + FreeCADGui.Control.closeDialog() + FreeCAD.ActiveDocument.recompute() + def optimized_cut(L_total, piezas, margen=0, metodo='auto'): """ Encuentra la combinación óptima de piezas para minimizar el desperdicio, diff --git a/PVPlantTerrain.py b/PVPlantTerrain.py index 8043e0f..91f8bcc 100644 --- a/PVPlantTerrain.py +++ b/PVPlantTerrain.py @@ -73,6 +73,42 @@ line_patterns = { "Dot (.5x) ...............................": 0x5555, "Dot (2x) . . . . . . . . . . .": 0x8888} + +def open_xyz_mmap(archivo_path): + """ + Usa memory-mapping para archivos muy grandes (máxima velocidad) + """ + # Primera pasada: contar líneas válidas + total_puntos = 0 + with open(archivo_path, 'r') as f: + for linea in f: + partes = linea.strip().split() + if len(partes) >= 3: + try: + float(partes[0]); + float(partes[1]); + float(partes[2]) + total_puntos += 1 + except: + continue + + # Segunda pasada: cargar datos + puntos = np.empty((total_puntos, 3)) + idx = 0 + + with open(archivo_path, 'r') as f: + for linea in f: + partes = linea.strip().split() + if len(partes) >= 3: + try: + x, y, z = float(partes[0]), float(partes[1]), float(partes[2]) + puntos[idx] = [x, y, z] + idx += 1 + except: + continue + + return puntos + def makeTerrain(name="Terrain"): obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Terrain") obj.Label = name @@ -81,7 +117,6 @@ def makeTerrain(name="Terrain"): FreeCAD.ActiveDocument.recompute() return obj - class Terrain(ArchComponent.Component): "A Shadow Terrain Obcject" @@ -161,101 +196,110 @@ class Terrain(ArchComponent.Component): if prop == "DEM" or prop == "CuttingBoundary": from datetime import datetime if obj.DEM and obj.CuttingBoundary: - ''' - Parámetro Descripción Requisitos - NCOLS: Cantidad de columnas de celdas Entero mayor que 0. - NROWS: Cantidad de filas de celdas Entero mayor que 0. - XLLCENTER o XLLCORNER: Coordenada X del origen (por el centro o la esquina inferior izquierda de la celda) Hacer coincidir con el tipo de coordenada y. - YLLCENTER o YLLCORNER: Coordenada Y del origen (por el centro o la esquina inferior izquierda de la celda) Hacer coincidir con el tipo de coordenada x. - CELLSIZE: Tamaño de celda Mayor que 0. - NODATA_VALUE: Los valores de entrada que serán NoData en el ráster de salida Opcional. El valor predeterminado es -9999 - ''' - grid_space = 1 - file = open(obj.DEM, "r") - templist = [line.split() for line in file.readlines()] - file.close() - del file + from pathlib import Path + suffix = Path(obj.DEM).suffix + if suffix == '.asc': + ''' + ASC format: + + Parámetro Descripción Requisitos + NCOLS: Cantidad de columnas de celdas Entero mayor que 0. + NROWS: Cantidad de filas de celdas Entero mayor que 0. + XLLCENTER o XLLCORNER: Coordenada X del origen (por el centro o la esquina inferior izquierda de la celda) Hacer coincidir con el tipo de coordenada y. + YLLCENTER o YLLCORNER: Coordenada Y del origen (por el centro o la esquina inferior izquierda de la celda) Hacer coincidir con el tipo de coordenada x. + CELLSIZE: Tamaño de celda Mayor que 0. + NODATA_VALUE: Los valores de entrada que serán NoData en el ráster de salida Opcional. El valor predeterminado es -9999 + ''' + grid_space = 1 + file = open(obj.DEM, "r") + templist = [line.split() for line in file.readlines()] + file.close() + del file - # Read meta data: - meta = templist[0:6] - nx = int(meta[0][1]) # NCOLS - ny = int(meta[1][1]) # NROWS - xllref = meta[2][0] # XLLCENTER / XLLCORNER - xllvalue = round(float(meta[2][1]), 3) - yllref = meta[3][0] # YLLCENTER / XLLCORNER - yllvalue = round(float(meta[3][1]), 3) - cellsize = round(float(meta[4][1]), 3) # CELLSIZE - nodata_value = float(meta[5][1]) # NODATA_VALUE + # Read meta data: + meta = templist[0:6] + nx = int(meta[0][1]) # NCOLS + ny = int(meta[1][1]) # NROWS + xllref = meta[2][0] # XLLCENTER / XLLCORNER + xllvalue = round(float(meta[2][1]), 3) + yllref = meta[3][0] # YLLCENTER / XLLCORNER + yllvalue = round(float(meta[3][1]), 3) + cellsize = round(float(meta[4][1]), 3) # CELLSIZE + nodata_value = float(meta[5][1]) # NODATA_VALUE - # set coarse_factor - coarse_factor = max(round(grid_space / cellsize), 1) + # set coarse_factor + coarse_factor = max(round(grid_space / cellsize), 1) - # Get z values - templist = templist[6:(6 + ny)] - templist = [templist[i][0::coarse_factor] for i in np.arange(0, len(templist), coarse_factor)] - datavals = np.array(templist).astype(float) - del templist + # Get z values + templist = templist[6:(6 + ny)] + templist = [templist[i][0::coarse_factor] for i in np.arange(0, len(templist), coarse_factor)] + datavals = np.array(templist).astype(float) + del templist - # create xy coordinates - offset = self.site.Origin - x = (cellsize * np.arange(nx)[0::coarse_factor] + xllvalue) * 1000 - offset.x - y = (cellsize * np.arange(ny)[-1::-1][0::coarse_factor] + yllvalue) * 1000 - offset.y - datavals = datavals * 1000 # Ajuste de altura + # create xy coordinates + offset = self.site.Origin + x = (cellsize * np.arange(nx)[0::coarse_factor] + xllvalue) * 1000 - offset.x + y = (cellsize * np.arange(ny)[-1::-1][0::coarse_factor] + yllvalue) * 1000 - offset.y + datavals = datavals * 1000 # Ajuste de altura - # remove points out of area - # 1. coarse: - if obj.CuttingBoundary: - inc_x = obj.CuttingBoundary.Shape.BoundBox.XLength * 0.0 - inc_y = obj.CuttingBoundary.Shape.BoundBox.YLength * 0.0 - tmp = np.where(np.logical_and(x >= (obj.CuttingBoundary.Shape.BoundBox.XMin - inc_x), - x <= (obj.CuttingBoundary.Shape.BoundBox.XMax + inc_x)))[0] - x_max = np.ndarray.max(tmp) - x_min = np.ndarray.min(tmp) + # remove points out of area + # 1. coarse: + if obj.CuttingBoundary: + inc_x = obj.CuttingBoundary.Shape.BoundBox.XLength * 0.0 + inc_y = obj.CuttingBoundary.Shape.BoundBox.YLength * 0.0 + tmp = np.where(np.logical_and(x >= (obj.CuttingBoundary.Shape.BoundBox.XMin - inc_x), + x <= (obj.CuttingBoundary.Shape.BoundBox.XMax + inc_x)))[0] + x_max = np.ndarray.max(tmp) + x_min = np.ndarray.min(tmp) - tmp = np.where(np.logical_and(y >= (obj.CuttingBoundary.Shape.BoundBox.YMin - inc_y), - y <= (obj.CuttingBoundary.Shape.BoundBox.YMax + inc_y)))[0] - y_max = np.ndarray.max(tmp) - y_min = np.ndarray.min(tmp) - del tmp + tmp = np.where(np.logical_and(y >= (obj.CuttingBoundary.Shape.BoundBox.YMin - inc_y), + y <= (obj.CuttingBoundary.Shape.BoundBox.YMax + inc_y)))[0] + y_max = np.ndarray.max(tmp) + y_min = np.ndarray.min(tmp) + del tmp - x = x[x_min:x_max+1] - y = y[y_min:y_max+1] - datavals = datavals[y_min:y_max+1, x_min:x_max+1] + x = x[x_min:x_max+1] + y = y[y_min:y_max+1] + datavals = datavals[y_min:y_max+1, x_min:x_max+1] - # Create mesh - surface: - import MeshTools.Triangulation as Triangulation - import Mesh - stepsize = 75 - stepx = math.ceil(nx / stepsize) - stepy = math.ceil(ny / stepsize) + # Create mesh - surface: + import MeshTools.Triangulation as Triangulation + import Mesh + stepsize = 75 + stepx = math.ceil(nx / stepsize) + stepy = math.ceil(ny / stepsize) - mesh = Mesh.Mesh() - for indx in range(stepx): - inix = indx * stepsize - 1 - finx = min([stepsize * (indx + 1), len(x)-1]) - for indy in range(stepy): - iniy = indy * stepsize - 1 - finy = min([stepsize * (indy + 1), len(y) - 1]) - pts = [] - for i in range(inix, finx): - for j in range(iniy, finy): - if datavals[j][i] != nodata_value: - if obj.CuttingBoundary: - if obj.CuttingBoundary.Shape.isInside(FreeCAD.Vector(x[i], y[j], 0), 0, True): + mesh = Mesh.Mesh() + for indx in range(stepx): + inix = indx * stepsize - 1 + finx = min([stepsize * (indx + 1), len(x)-1]) + for indy in range(stepy): + iniy = indy * stepsize - 1 + finy = min([stepsize * (indy + 1), len(y) - 1]) + pts = [] + for i in range(inix, finx): + for j in range(iniy, finy): + if datavals[j][i] != nodata_value: + if obj.CuttingBoundary: + if obj.CuttingBoundary.Shape.isInside(FreeCAD.Vector(x[i], y[j], 0), 0, True): + pts.append([x[i], y[j], datavals[j][i]]) + else: pts.append([x[i], y[j], datavals[j][i]]) - else: - pts.append([x[i], y[j], datavals[j][i]]) - if len(pts) > 3: - try: - triangulated = Triangulation.Triangulate(pts) - mesh.addMesh(triangulated) - except TypeError: - print(f"Error al procesar {len(pts)} puntos: {str(e)}") + if len(pts) > 3: + try: + triangulated = Triangulation.Triangulate(pts) + mesh.addMesh(triangulated) + except TypeError: + print(f"Error al procesar {len(pts)} puntos: {str(e)}") + + mesh.removeDuplicatedPoints() + mesh.removeFoldsOnSurface() + obj.InitialMesh = mesh.copy() + Mesh.show(mesh) + elif suffix in ['.xyz']: + data = open_xyz_mmap(obj.DEM) + - mesh.removeDuplicatedPoints() - mesh.removeFoldsOnSurface() - obj.InitialMesh = mesh.copy() - Mesh.show(mesh) if prop == "PointsGroup" or prop == "CuttingBoundary": if obj.PointsGroup and obj.CuttingBoundary: diff --git a/Project/Area/PVPlantArea.py b/Project/Area/PVPlantArea.py index 67227cc..f6eb818 100644 --- a/Project/Area/PVPlantArea.py +++ b/Project/Area/PVPlantArea.py @@ -26,6 +26,9 @@ import PVPlantSite import Utils.PVPlantUtils as utils import MeshPart as mp +import pivy +from pivy import coin + if FreeCAD.GuiUp: import FreeCADGui from DraftTools import translate @@ -361,12 +364,12 @@ class OffsetArea(_Area): wire = utils.getProjected(base, vec) wire = wire.makeOffset2D(obj.OffsetDistance.Value, 2, False, False, True) sections = mp.projectShapeOnMesh(wire, land, vec) + print(" javi ", sections) pts = [] for section in sections: pts.extend(section) - # Crear forma solo si hay resultados - if sections: + if len(pts)>0: obj.Shape = Part.makePolygon(pts) else: obj.Shape = Part.Shape() # Forma vacía si falla @@ -412,35 +415,9 @@ class ProhibitedArea(OffsetArea): self.Type = obj.Type = "ProhibitedArea" obj.Proxy = self - '''# Propiedades de color - if not hasattr(obj, "OriginalColor"): - obj.addProperty("App::PropertyColor", - "OriginalColor", - "Display", - "Color for original wire") - obj.OriginalColor = (1.0, 0.0, 0.0) # Rojo - - if not hasattr(obj, "OffsetColor"): - obj.addProperty("App::PropertyColor", - "OffsetColor", - "Display", - "Color for offset wire") - obj.OffsetColor = (1.0, 0.5, 0.0) # Naranja - - # Propiedades de grosor - if not hasattr(obj, "OriginalWidth"): - obj.addProperty("App::PropertyFloat", - "OriginalWidth", - "Display", - "Line width for original wire") - obj.OriginalWidth = 4.0 - - if not hasattr(obj, "OffsetWidth"): - obj.addProperty("App::PropertyFloat", - "OffsetWidth", - "Display", - "Line width for offset wire") - obj.OffsetWidth = 4.0''' + def onDocumentRestored(self, obj): + """Method run when the document is restored.""" + self.setProperties(obj) def execute(self, obj): # Comprobar dependencias @@ -482,121 +459,402 @@ class ProhibitedArea(OffsetArea): obj.Shape = Part.Shape() # Actualizar colores en la vista - if FreeCAD.GuiUp and obj.ViewObject: - obj.ViewObject.Proxy.updateVisual() + """if FreeCAD.GuiUp and obj.ViewObject: + obj.ViewObject.Proxy.updateVisual()""" -class ViewProviderForbiddenArea(_ViewProviderArea): +class ViewProviderForbiddenArea_old: def __init__(self, vobj): - super().__init__(vobj) - # Valores por defecto - self.original_color = (1.0, 0.0, 0.0) # Rojo - self.offset_color = (1.0, 0.5, 0.0) # Naranja - self.original_width = 4.0 - self.offset_width = 4.0 - self.line_widths = [] # Almacenará los grosores por arista + vobj.Proxy = self + self.setProperties(vobj) - vobj.LineColor = (1.0, 0.0, 0.0) - vobj.LineWidth = 4 - vobj.PointColor = (1.0, 0.0, 0.0) - vobj.PointSize = 4 + def setProperties(self, vobj): + # Propiedades de color + if not hasattr(vobj, "OriginalColor"): + vobj.addProperty("App::PropertyColor", + "OriginalColor", + "ObjectStyle", + "Color for original wire") + vobj.OriginalColor = (1.0, 0.0, 0.0) # Rojo - def getIcon(self): - ''' Return object treeview icon. ''' - return str(os.path.join(DirIcons, "area_forbidden.svg")) + if not hasattr(vobj, "OffsetColor"): + vobj.addProperty("App::PropertyColor", + "OffsetColor", + "ObjectStyle", + "Color for offset wire") + vobj.OffsetColor = (1.0, 0.0, 0.0) # Rojo - def claimChildren(self): - """ Provides object grouping """ - children = [] - if self.ViewObject and self.ViewObject.Object.Base: - children.append(self.ViewObject.Object.Base) - return children + # Propiedades de grosor + if not hasattr(vobj, "OriginalWidth"): + vobj.addProperty("App::PropertyFloat", + "OriginalWidth", + "ObjectStyle", + "Line width for original wire") + vobj.OriginalWidth = 4.0 + + if not hasattr(vobj, "OffsetWidth"): + vobj.addProperty("App::PropertyFloat", + "OffsetWidth", + "ObjectStyle", + "Line width for offset wire") + vobj.OffsetWidth = 4.0 + + # Deshabilitar el color por defecto + vobj.setPropertyStatus("LineColor", "Hidden") + vobj.setPropertyStatus("PointColor", "Hidden") + vobj.setPropertyStatus("ShapeAppearance", "Hidden") def attach(self, vobj): - super().attach(vobj) - # Inicializar visualización - self.updateVisual() + self.ViewObject = vobj + self.Object = vobj.Object - def updateVisual(self): - """Actualiza colores y grosores de línea""" - if not hasattr(self, 'ViewObject') or not self.ViewObject or not self.ViewObject.Object: - return + # Crear la estructura de escena Coin3D + self.root = coin.SoGroup() - obj = self.ViewObject.Object + # Switch para habilitar/deshabilitar la selección + self.switch = coin.SoSwitch() + self.switch.whichChild = coin.SO_SWITCH_ALL - # Obtener propiedades de color y grosor - try: - self.original_color = obj.OriginalColor - self.offset_color = obj.OffsetColor - self.original_width = obj.OriginalWidth - self.offset_width = obj.OffsetWidth - except: - pass + # Separador para el wire original + self.original_sep = coin.SoSeparator() + self.original_color = coin.SoBaseColor() + self.original_coords = coin.SoCoordinate3() + self.original_line_set = coin.SoLineSet() + self.original_draw_style = coin.SoDrawStyle() - # Actualizar colores si hay forma - if hasattr(obj, 'Shape') and obj.Shape and not obj.Shape.isNull(): - if len(obj.Shape.SubShapes) >= 2: - # Asignar colores - colors = [] - colors.append(self.original_color) # Primer wire (original) - colors.append(self.offset_color) # Segundo wire (offset) - self.ViewObject.DiffuseColor = colors + # Separador para el wire offset + self.offset_sep = coin.SoSeparator() + self.offset_color = coin.SoBaseColor() + self.offset_coords = coin.SoCoordinate3() + self.offset_line_set = coin.SoLineSet() + self.offset_draw_style = coin.SoDrawStyle() - # Preparar grosores por arista - #self.prepareLineWidths() + # Construir la jerarquía de escena + self.original_sep.addChild(self.original_color) + self.original_sep.addChild(self.original_draw_style) + self.original_sep.addChild(self.original_coords) + self.original_sep.addChild(self.original_line_set) - # Asignar grosores usando LineWidthArray - '''if self.line_widths: - self.ViewObject.LineWidthArray = self.line_widths''' + self.offset_sep.addChild(self.offset_color) + self.offset_sep.addChild(self.offset_draw_style) + self.offset_sep.addChild(self.offset_coords) + self.offset_sep.addChild(self.offset_line_set) - # Establecer grosor global como respaldo - #self.ViewObject.LineWidth = max(self.original_width, self.offset_width) + self.switch.addChild(self.original_sep) + self.switch.addChild(self.offset_sep) + self.root.addChild(self.switch) - def prepareLineWidths(self): - """Prepara la lista de grosores para cada arista""" - self.line_widths = [] - obj = self.ViewObject.Object + vobj.addDisplayMode(self.root, "Wireframe") - if hasattr(obj, 'Shape') and obj.Shape and not obj.Shape.isNull(): - # Contar aristas en cada subforma - for i, subshape in enumerate(obj.Shape.SubShapes): - edge_count = len(subshape.Edges) if hasattr(subshape, 'Edges') else 1 + # Inicializar estilos de dibujo + self.original_draw_style.style = coin.SoDrawStyle.LINES + self.offset_draw_style.style = coin.SoDrawStyle.LINES - # Determinar grosor según tipo de wire - width = self.original_width if i == 0 else self.offset_width - - # Asignar el mismo grosor a todas las aristas de este wire - self.line_widths.extend([width] * edge_count) - - def onChanged(self, vobj, prop): - """Maneja cambios en propiedades de visualización""" - if prop in ["LineColor", "PointColor", "ShapeColor", "LineWidth"]: + # Actualizar visualización inicial + if hasattr(self.Object, 'Shape'): + self.updateData(self.Object, "Shape") self.updateVisual() def updateData(self, obj, prop): - """Actualiza cuando cambian los datos del objeto""" - if prop == "Shape": + if prop == "Shape" and obj.Shape and not obj.Shape.isNull(): + self.updateGeometry() + + def updateGeometry(self): + """Actualiza la geometría en la escena 3D""" + if not hasattr(self, 'Object') or not self.Object.Shape or self.Object.Shape.isNull(): + return + + # Limpiar coordenadas existentes + self.original_coords.point.deleteValues(0) + self.offset_coords.point.deleteValues(0) + + # Obtener los sub-shapes + subshapes = [] + if hasattr(self.Object.Shape, 'SubShapes') and self.Object.Shape.SubShapes: + subshapes = self.Object.Shape.SubShapes + elif hasattr(self.Object.Shape, 'ChildShapes') and self.Object.Shape.ChildShapes: + subshapes = self.Object.Shape.ChildShapes + + # Procesar wire original (primer sub-shape) + if len(subshapes) > 0: + self.processShape(subshapes[0], self.original_coords, self.original_line_set) + + # Procesar wire offset (segundo sub-shape) + if len(subshapes) > 1: + self.processShape(subshapes[1], self.offset_coords, self.offset_line_set) + + # Actualizar colores y grosores + self.updateVisual() + + def processShape(self, shape, coords_node, lineset_node): + """Procesa una forma y la añade al nodo de coordenadas""" + if not shape or shape.isNull(): + return + + points = [] + line_indices = [] + current_index = 0 + + # Obtener todos los edges de la forma + edges = [] + if hasattr(shape, 'Edges'): + edges = shape.Edges + elif hasattr(shape, 'ChildShapes'): + for child in shape.ChildShapes: + if hasattr(child, 'Edges'): + edges.extend(child.Edges) + + for edge in edges: + try: + # Discretizar la curva para obtener puntos + vertices = edge.discretize(Number=50) + + for i, vertex in enumerate(vertices): + points.append([vertex.x, vertex.y, vertex.z]) + line_indices.append(current_index) + current_index += 1 + + # Añadir -1 para indicar fin de línea + line_indices.append(-1) + + except Exception as e: + print(f"Error processing edge: {e}") + continue + + # Configurar coordenadas y líneas + if points: + coords_node.point.setValues(0, len(points), points) + lineset_node.numVertices.deleteValues(0) + lineset_node.numVertices.setValues(0, len(line_indices), line_indices) + + def updateVisual(self): + """Actualiza colores y grosores según las propiedades""" + if not hasattr(self, 'ViewObject') or not self.ViewObject: + return + + vobj = self.ViewObject + + try: + # Configurar wire original + if hasattr(vobj, "OriginalColor"): + original_color = vobj.OriginalColor + self.original_color.rgb.setValue(original_color[0], original_color[1], original_color[2]) + + if hasattr(vobj, "OriginalWidth"): + self.original_draw_style.lineWidth = vobj.OriginalWidth + + # Configurar wire offset + if hasattr(vobj, "OffsetColor"): + offset_color = vobj.OffsetColor + self.offset_color.rgb.setValue(offset_color[0], offset_color[1], offset_color[2]) + + if hasattr(vobj, "OffsetWidth"): + self.offset_draw_style.lineWidth = vobj.OffsetWidth + + except Exception as e: + print(f"Error updating visual: {e}") + + def onChanged(self, vobj, prop): + """Maneja cambios en propiedades""" + if prop in ["OriginalColor", "OffsetColor", "OriginalWidth", "OffsetWidth"]: self.updateVisual() - '''def __getstate__(self): - return { - "original_color": self.original_color, - "offset_color": self.offset_color, - "original_width": self.original_width, - "offset_width": self.offset_width - } + def getDisplayModes(self, obj): + return ["Wireframe"] + + def getDefaultDisplayMode(self): + return "Wireframe" + + def setDisplayMode(self, mode): + return mode + + def claimChildren(self): + """Proporciona agrupamiento de objetos""" + children = [] + if hasattr(self, 'Object') and self.Object and hasattr(self.Object, "Base"): + children.append(self.Object.Base) + return children + + def getIcon(self): + '''Return object treeview icon''' + return str(os.path.join(DirIcons, "area_forbidden.svg")) + + def onDocumentRestored(self, vobj): + """Método ejecutado cuando el documento es restaurado""" + self.ViewObject = vobj + self.Object = vobj.Object + self.setProperties(vobj) + self.attach(vobj) + + def __getstate__(self): + return None def __setstate__(self, state): - if "original_color" in state: - self.original_color = state["original_color"] - if "offset_color" in state: - self.offset_color = state["offset_color"] - if "original_width" in state: - self.original_width = state.get("original_width", 4.0) - if "offset_width" in state: - self.offset_width = state.get("offset_width", 4.0)''' + return None +class ViewProviderForbiddenArea: + def __init__(self, vobj): + vobj.Proxy = self + self.ViewObject = vobj + + # Inicializar propiedades PRIMERO + self.setProperties(vobj) + + # Configurar colores iniciales + self.updateColors(vobj) + + def setProperties(self, vobj): + if not hasattr(vobj, "OriginalColor"): + vobj.addProperty("App::PropertyColor", + "OriginalColor", + "Display", + "Color for original wire") + vobj.OriginalColor = (1.0, 0.0, 0.0) # Rojo + + if not hasattr(vobj, "OffsetColor"): + vobj.addProperty("App::PropertyColor", + "OffsetColor", + "Display", + "Color for offset wire") + vobj.OffsetColor = (1.0, 0.5, 0.0) # Naranja + + def updateColors(self, vobj): + """Actualiza los colores desde las propiedades""" + try: + if hasattr(vobj, "OriginalColor"): + self.original_color.rgb.setValue(*vobj.OriginalColor) + else: + self.original_color.rgb.setValue(1.0, 0.0, 0.0) + + if hasattr(vobj, "OffsetColor"): + self.offset_color.rgb.setValue(*vobj.OffsetColor) + else: + self.offset_color.rgb.setValue(1.0, 0.5, 0.0) + except Exception as e: + print(f"Error en updateColors: {e}") + + def onDocumentRestored(self, vobj): + self.setProperties(vobj) + # No llamar a __init__ de nuevo, solo actualizar propiedades + self.updateColors(vobj) + + def getIcon(self): + return str(os.path.join(DirIcons, "area_forbidden.svg")) + + def attach(self, vobj): + self.ViewObject = vobj + + # Inicializar nodos Coin3D + self.root = coin.SoGroup() + self.original_coords = coin.SoCoordinate3() + self.offset_coords = coin.SoCoordinate3() + self.original_color = coin.SoBaseColor() + self.offset_color = coin.SoBaseColor() + self.original_lineset = coin.SoLineSet() + self.offset_lineset = coin.SoLineSet() + + # Añadir un nodo de dibujo para establecer el estilo de línea + self.draw_style = coin.SoDrawStyle() + self.draw_style.style = coin.SoDrawStyle.LINES + self.draw_style.lineWidth = 3.0 + + # Construir la escena + self.root.addChild(self.draw_style) + + # Grupo para el polígono original + original_group = coin.SoGroup() + original_group.addChild(self.original_color) + original_group.addChild(self.original_coords) + original_group.addChild(self.original_lineset) + + # Grupo para el polígono offset + offset_group = coin.SoGroup() + offset_group.addChild(self.offset_color) + offset_group.addChild(self.offset_coords) + offset_group.addChild(self.offset_lineset) + + self.root.addChild(original_group) + self.root.addChild(offset_group) + + vobj.addDisplayMode(self.root, "Standard") + # Asegurar que la visibilidad esté activada + vobj.Visibility = True + + def updateData(self, obj, prop): + if prop == "Shape": + self.updateVisual(obj) + + def updateVisual(self, obj): + """Actualiza la representación visual basada en la forma del objeto""" + if not hasattr(obj, 'Shape') or not obj.Shape or obj.Shape.isNull(): + return + + try: + # Obtener todos los bordes de la forma compuesta + all_edges = obj.Shape.Edges + + # Separar bordes por polígono (asumimos que el primer polígono es el original) + # Esto es una simplificación - podrías necesitar una lógica más sofisticada + if len(all_edges) >= 2: + # Polígono original - primer conjunto de bordes + original_edges = [all_edges[0]] + original_points = [] + for edge in original_edges: + for vertex in edge.Vertexes: + original_points.append((vertex.Point.x, vertex.Point.y, vertex.Point.z)) + + # Polígono offset - segundo conjunto de bordes + offset_edges = [all_edges[1]] + offset_points = [] + for edge in offset_edges: + for vertex in edge.Vertexes: + offset_points.append((vertex.Point.x, vertex.Point.y, vertex.Point.z)) + + # Asignar puntos a los nodos Coordinate3 + if original_points: + self.original_coords.point.setValues(0, len(original_points), original_points) + self.original_lineset.numVertices.setValue(len(original_points)) + + if offset_points: + self.offset_coords.point.setValues(0, len(offset_points), offset_points) + self.offset_lineset.numVertices.setValue(len(offset_points)) + + # Actualizar colores + if hasattr(obj, 'ViewObject') and obj.ViewObject: + self.updateColors(obj.ViewObject) + + except Exception as e: + print(f"Error en updateVisual: {e}") + + def onChanged(self, vobj, prop): + if prop in ["OriginalColor", "OffsetColor"]: + self.updateColors(vobj) + elif prop == "Visibility" and vobj.Visibility: + # Cuando la visibilidad cambia a True, actualizar visual + self.updateVisual(vobj.Object) + + def getDisplayModes(self, obj): + return ["Standard"] + + def getDefaultDisplayMode(self): + return "Standard" + + def setDisplayMode(self, mode): + return mode + + def claimChildren(self): + children = [] + if hasattr(self, 'ViewObject') and self.ViewObject and hasattr(self.ViewObject.Object, 'Base'): + children.append(self.ViewObject.Object.Base) + return children + + def dumps(self): + return None + + def loads(self, state): + return None + ''' PV Area: ''' def makePVSubplant(): obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "PVSubplant") diff --git a/package.xml b/package.xml index 4adc7ac..4c88ea8 100644 --- a/package.xml +++ b/package.xml @@ -8,11 +8,12 @@ LGPL-2.1-or-later https://homehud.duckdns.org/javier/PVPlant https://homehud.duckdns.org/javier/PVPlant/issues + https://homehud.duckdns.org/javier/PVPlant/src/branch/main/README.md PVPlant/Resources/Icons/PVPlantWorkbench.svg - RoadWorkbench + PVPlantWorkbench ./ -- 2.49.1 From 4476afc1a21b87cab0cfaeccc41d116b28da0ed3 Mon Sep 17 00:00:00 2001 From: Javi Date: Thu, 20 Nov 2025 11:20:18 +0100 Subject: [PATCH 6/6] updates --- package.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.xml b/package.xml index 4c88ea8..ced4267 100644 --- a/package.xml +++ b/package.xml @@ -2,13 +2,13 @@ PVPlant FreeCAD Fotovoltaic Power Plant Toolkit - 2025.07.06 - 2025.07.06 + 2025.11.20 + 2025.11.20 Javier Braña LGPL-2.1-or-later - https://homehud.duckdns.org/javier/PVPlant + https://homehud.duckdns.org/javier/PVPlant https://homehud.duckdns.org/javier/PVPlant/issues - https://homehud.duckdns.org/javier/PVPlant/src/branch/main/README.md + https://homehud.duckdns.org/javier/PVPlant/src/branch/developed/README.md PVPlant/Resources/Icons/PVPlantWorkbench.svg -- 2.49.1