From d61260fdd3174b84dbf2c4e90b3a9a1a8cc3bb67 Mon Sep 17 00:00:00 2001 From: Javi Date: Thu, 20 Nov 2025 00:57:15 +0100 Subject: [PATCH] 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 ./