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:
- 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
./