Files
PVPlant/PVPlantPlacement.py
2025-11-20 00:57:15 +01:00

2331 lines
90 KiB
Python

# /**********************************************************************
# * *
# * Copyright (c) 2021 Javier Braña <javier.branagutierrez@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify*
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307*
# * USA *
# * *
# ***********************************************************************
import FreeCAD
import Part
if FreeCAD.GuiUp:
import FreeCADGui, os
from PySide import QtCore, QtGui
from PySide.QtGui import QListWidgetItem
from PySide.QtCore import QT_TRANSLATE_NOOP
else:
# \cond
def translate(ctxt, txt):
return txt
def QT_TRANSLATE_NOOP(ctxt, txt):
return txt
# \endcond
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
def _fromUtf8(s):
return s
import PVPlantResources
import PVPlantSite
version = "0.1.0"
def selectionFilter(sel, objtype):
fil = []
for obj in sel:
if hasattr(obj, "Proxy"):
if obj.Proxy.__class__ is objtype:
fil.append(obj)
return fil
class _PVPlantPlacementTaskPanel_old:
'''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) # Norte a sur
# self.form:
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 len(sel) > 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
'''try:
MechanicalGroup = FreeCAD.ActiveDocument.Frames
except:
MechanicalGroup = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'Frames')
MechanicalGroup.Label = "Frames"
FreeCAD.ActiveDocument.MechanicalGroup.addObject(MechanicalGroup)
if self.form.cbSubfolders.isChecked:
name = "Frames-" + self.PVArea.Label
if name in [obj.Name for obj in FreeCAD.ActiveDocument.Frames.Group]:
MechanicalGroup = FreeCAD.ActiveDocument.getObject(name)[0]
else:
group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", name)
group.Label = name
MechanicalGroup.addObject(group)
MechanicalGroup = group'''
doc = FreeCAD.ActiveDocument
# 1. Obtener o crear el grupo principal 'Frames'
main_group_name = "Frames"
main_group = doc.getObject(main_group_name)
if not main_group:
main_group = doc.addObject("App::DocumentObjectGroup", main_group_name)
main_group.Label = main_group_name
# Asumiendo que existe un grupo 'MechanicalGroup'
if hasattr(doc, 'MechanicalGroup'):
doc.MechanicalGroup.addObject(main_group)
# 2. Manejar subgrupo si es necesario
group = main_group # Grupo donde se añadirán los marcos
if self.form.cbSubfolders.isChecked(): # ¡Corregido: falta de paréntesis!
subgroup_name = f"Frames-{self.PVArea.Label}"
# Buscar subgrupo existente
subgroup = next((obj for obj in main_group.Group if obj.Name == subgroup_name), None)
if not subgroup:
subgroup = doc.addObject("App::DocumentObjectGroup", subgroup_name)
subgroup.Label = subgroup_name
main_group.addObject(subgroup)
group = subgroup
try:
placements = dataframe["placement"].tolist()
types = dataframe["type"].tolist()
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(cols)
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()
class _PVPlantPlacementTaskPanel_new1:
'''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) # Norte a sur
# self.form:
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 len(sel) > 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,
considerando un margen entre piezas.
Args:
L_total (int): Longitud total del material.
piezas (list): Lista de longitudes de los patrones de corte.
margen (int): Espacio perdido entre piezas consecutivas.
metodo (str): 'dp' para programación dinámica, 'greedy' para voraz, 'auto' para selección automática.
Returns:
tuple: (piezas_seleccionadas, desperdicio)
"""
# Filtrar piezas inválidas
piezas = [p for p in piezas if 0 < p <= L_total]
if not piezas:
return [], L_total
# Transformar longitudes y longitud total con margen
longitudes_aumentadas = [p + margen for p in piezas]
L_total_aumentado = L_total + margen
# Selección automática de método
if metodo == 'auto':
if L_total_aumentado <= 10000 and len(piezas) <= 100:
metodo = 'dp'
else:
metodo = 'greedy'
if metodo == 'dp':
n = len(piezas)
dp = [0] * (L_total_aumentado + 1)
parent = [-1] * (L_total_aumentado + 1) # Almacena índices de piezas usadas
# Llenar la tabla dp y parent
for j in range(1, L_total_aumentado + 1):
for i in range(n):
p_aum = longitudes_aumentadas[i]
if p_aum <= j:
if dp[j] < dp[j - p_aum] + p_aum:
dp[j] = dp[j - p_aum] + p_aum
parent[j] = i # Guardar índice de la pieza
# Reconstruir solución desde el final
current = L_total_aumentado
seleccion_indices = []
while current > 0 and parent[current] != -1:
i = parent[current]
seleccion_indices.append(i)
current -= longitudes_aumentadas[i]
# Calcular desperdicio real
k = len(seleccion_indices)
if k == 0:
desperdicio = L_total
else:
suma_original = sum(piezas[i] for i in seleccion_indices)
desperdicio = L_total - suma_original - margen * (k - 1)
return seleccion_indices, desperdicio
elif metodo == 'greedy':
# Crear lista con índices y longitudes aumentadas
lista_con_indices = [(longitudes_aumentadas[i], i) for i in range(len(piezas))]
lista_con_indices.sort(key=lambda x: x[0], reverse=True) # Ordenar descendente
seleccion_indices = []
restante = L_total_aumentado
# Seleccionar piezas vorazmente
for p_aum, i in lista_con_indices:
while restante >= p_aum:
seleccion_indices.append(i)
restante -= p_aum
# Calcular desperdicio real
k = len(seleccion_indices)
if k == 0:
desperdicio = L_total
else:
suma_original = sum(piezas[i] for i in seleccion_indices)
desperdicio = L_total - suma_original - margen * (k - 1)
return seleccion_indices, desperdicio
# Ejemplo de uso
'''if __name__ == "__main__":
L_total = 100
piezas = [25, 35, 40, 20, 15, 30, 50]
margen = 5
print("Solución óptima con margen (programación dinámica):")
seleccion, desperd = corte_optimizado(L_total, piezas, margen, 'dp')
print(f"Piezas usadas: {seleccion}")
print(f"Margen entre piezas: {margen} cm")
print(f"Material útil: {sum(seleccion)} cm")
print(f"Espacio usado por márgenes: {(len(seleccion) - 1) * margen} cm")
print(f"Desperdicio total: {desperd} cm")
print("\nSolución aproximada con margen (algoritmo voraz):")
seleccion_g, desperd_g = corte_optimizado(L_total, piezas, margen, 'greedy')
print(f"Piezas usadas: {seleccion_g}")
print(f"Margen entre piezas: {margen} cm")
print(f"Material útil: {sum(seleccion_g)} cm")
print(f"Espacio usado por márgenes: {(len(seleccion_g) - 1) * margen} cm")
print(f"Desperdicio total: {desperd_g} cm")'''
# ----------------------------------------------------------------------------------------------------------------------
# function AdjustToTerrain
# Take a group of objects and adjust it to the slope and altitude of the terrain mesh. It detects the terrain mesh
#
# Inputs:
# 1. frames: group of objest to adjust
# ----------------------------------------------------------------------------------------------------------------------
class adjustToTerrainTaskPanel:
def __init__(self, obj=None):
self.obj = obj
self.form = FreeCADGui.PySideUic.loadUi(PVPlantResources.__dir__ + "/PVPlantPlacementAdjust.ui")
def accept(self):
frames = []
for obj in FreeCADGui.Selection.getSelection():
if obj.Name.startswith("Tracker"):
frames.append(obj)
elif obj.Name.startswith("FrameArea"):
frames.extend(obj.Frames)
adjustToTerrain(frames, self.form.comboMethod.currentIndex() == 0)
self.close()
return True
def reject(self):
self.close()
return False
def close(self):
FreeCADGui.Control.closeDialog()
import numpy as np
from scipy import stats
def get_trend(points):
"""Return the trend of a list of 3D points"""
x, y, z = zip(*[(point.x, point.y, point.z) for point in points])
slope, intercept, _, _, _ = stats.linregress(y, z)
new_z = slope * np.array([y[0], y[-1]]) + intercept
return [FreeCAD.Vector(x[0], y[0], new_z[0]), FreeCAD.Vector(x[-1], y[-1], new_z[1])]
def getTrend(points): # old
from scipy import stats
def getNewZ(x):
return slope * x + intercept
xx = list()
yy = list()
zz = list()
for point in points:
xx.append(point.x)
yy.append(point.y)
zz.append(point.z)
slope, intercept, r, p, std_err = stats.linregress(yy, zz)
newzz = list(map(getNewZ, [yy[0], yy[-1]]))
return [FreeCAD.Vector(xx[0], yy[0], newzz[0]),
FreeCAD.Vector(xx[-1], yy[-1], newzz[1])]
def adjustToTerrain(frames, individual=True):
from datetime import datetime
starttime = datetime.now()
import MeshPart as mp
FreeCAD.ActiveDocument.openTransaction("Adjust to terrain")
terrain = PVPlantSite.get().Terrain.Mesh
if individual:
for frame in frames:
length = frame.Setup.Length.Value / 2 + 5000
p1 = FreeCAD.Vector(-length, 0, 0, )
p2 = FreeCAD.Vector(length, 0, 0, )
line = Part.LineSegment(p1, p2).toShape()
line.Placement = frame.Placement.copy()
line.Placement.Base.z = 0
xyz = line.Placement.Rotation.toEulerAngles("XYZ")
line.Placement.Rotation.setEulerAngles("XYZ", 0, 0, xyz[2])
pro = mp.projectShapeOnMesh(line, terrain, FreeCAD.Vector(0, 0, 1))
pts = []
for points in pro:
pts.extend(points)
points3D = get_trend(pts)
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)
frame.Placement = pl
else:
import math
def getLineAngle(line):
# ángulo en grados = arctan(ángulo en porcentaje / 100%)
import math
p1 = FreeCAD.Vector(line.Vertexes[0].Point)
p2 = FreeCAD.Vector(line.Vertexes[1].Point)
hi = p2.z - p1.z
p1.z = 0
p2.z = 0
le = p2.sub(p1).Length
return math.degrees(math.atan2(hi, le))
cols = getCols(frames)
for col in cols:
for group in col:
# Ver 1 -----------------
lines = []
# 1. Generar las líneas de trabajo.
for frame in group:
# 1.1. Corregir los frames que estén fuera de tolerancia:
if frame.AngleY < FreeCAD.ActiveDocument.MaximumTiltNegative.Value:
frame.AngleY = FreeCAD.ActiveDocument.MaximumTiltNegative.Value
if frame.AngleY > FreeCAD.ActiveDocument.MaximumTiltPositive.Value:
frame.AngleY = FreeCAD.ActiveDocument.MaximumTiltPositive.Value
# 1.2. Generar las líneas con las que se trabajarán:
l = frame.Setup.Length / 2
pn = FreeCAD.Vector(-l, 0, 0)
ps = FreeCAD.Vector( l, 0, 0)
line = Part.LineSegment(pn, ps).toShape()
line.Placement = frame.Placement.copy()
lines.append([frame, line])
# 2. Poner los tracker en tolerancia:
cnt = len(lines)
if cnt > 1:
angleLine=[]
anglesTwoLines=[]
for frame in lines:
angleLine.append(frame[0].AngleY.Value)
for ind in range(cnt - 1):
frame1 = lines[ind]
frame2 = lines[ind + 1]
vec1 = frame1[1].Vertexes[1].Point.sub(frame1[1].Vertexes[0].Point)
vec2 = frame2[1].Vertexes[1].Point.sub(frame2[1].Vertexes[0].Point)
anglesTwoLines.append(math.degrees(vec2.getAngle(vec1)))
print(angleLine)
print(anglesTwoLines)
pass
for ind, frame in enumerate(lines):
frame0 = None
frame1 = None
if ind > 0:
frame0 = lines[ind - 1]
if ind < (len(group) - 1):
frame1 = lines[ind + 1]
if (frame0 is None) and (frame1 is None): # Caso 1: sólo 1 frame por fila
# no se hace nada. ya está con todos los parámetros dentro de tolerancia
pass
elif (frame0 is None) and not (frame1 is None): # Caso 2: frame es el primero y hay más frames
pass
elif not (frame0 is None) and (frame1 is None): # Caso 3: el frame es el último y hay más frames
pass
else: # Caso 4: el frame está en el médio de varios frames
pass
continue
# Ver 0 -----------------
points = []
# 1. Get lines/points to project on land
frame1 = group[0] # Norte
frame2 = group[-1] # Sur
# 1.1. Get the first and last points:
# TODO: revisar esta parte:
p0 = FreeCAD.Vector(frame1.Shape.BoundBox.Center.x, frame1.Shape.BoundBox.YMax, 0.0)
pf = FreeCAD.Vector(frame2.Shape.BoundBox.Center.x, frame2.Shape.BoundBox.YMin, 0.0)
vec = (pf - p0).normalize()
points.append(p0)
for ind in range(0, len(group) - 1):
frame1 = group[ind]
frame2 = group[ind + 1]
vec1 = FreeCAD.Vector(frame1.Placement.Base)
vec2 = FreeCAD.Vector(frame2.Placement.Base)
vec1.z = 0
vec2.z = 0
vec3 = vec2.sub(vec1)
c = vec3.Length / 2 + (frame1.Setup.Length.Value - frame2.Setup.Length.Value) / 4
v = FreeCAD.Vector(vec)
v.Length = c
v = vec1.add(v)
v.z = 0
points.append(v)
points.append(pf)
# 2. Calculate trend:
points3D = []
for ind in range(len(points) - 1):
line = Part.LineSegment(points[ind], points[ind + 1]).toShape()
pro = mp.projectShapeOnMesh(line, terrain, FreeCAD.Vector(0, 0, 1))
pts = []
for lp in pro:
pts.extend(lp)
points3D.extend(get_trend(pts))
# Todo: aplicar aproximación de los vertices:
# prueba:
for i in range(0, len(points3D) - 2, 2):
# p0 = points3D[i]
p1 = points3D[i + 1]
p2 = points3D[i + 2]
# p3 = points3D[i + 3]
l = p1.sub(p2).Length
if l > 250:
l = (l - 250) / 2
if p1.z > p2.z:
p1.z -= l
p2.z += l
else:
p1.z += l
p2.z -= l
# 3. Aplicar placement
for ind, frame in enumerate(group):
v1 = points3D[ind * 2]
v2 = points3D[ind * 2 + 1]
pl = frame.Placement.copy()
pl.Base.z = (v1.add(v2) / 2).z
rot = FreeCAD.Rotation(FreeCAD.Vector(-1, 0, 0), v1.sub(v2))
pl.Rotation = FreeCAD.Rotation(rot.toEuler()[0], rot.toEuler()[1], 0)
frame.Placement = pl
FreeCAD.ActiveDocument.commitTransaction()
total_time = datetime.now() - starttime
print(" -- Tiempo tardado en ajustar al terreno:", total_time)
FreeCAD.activeDocument().recompute()
def getRows(objs):
''' '''
def countFrames(columns):
cnt = 0
for icol in columns:
cnt += len(icol)
return cnt
if len(objs) == 0:
return None, None
cols = getCols(list(objs))
tmpcols = []
for col in cols:
g = []
for group in col:
g.extend(group)
tmpcols.append(g)
rows = []
while countFrames(tmpcols) > 0:
firstCol = max(tmpcols, key=lambda col: col[0].Placement.Base.y)
compFrame = max(firstCol, key=lambda x: x.Placement.Base.y)
ind = tmpcols.index(firstCol)
group = [compFrame,]
tmpcols[ind].remove(compFrame)
for i in range(ind - 1, 0, -1):
if len(tmpcols[i]) == 0:
break
frame = tmpcols[i][0]
framelen = frame.Setup.Length / 2
compFramelen = compFrame.Setup.Length / 2
l = max([framelen, compFramelen])
if abs(compFrame.Placement.Base.y - frame.Placement.Base.y) <= l:
group.append(frame)
tmpcols[i].remove(frame)
compFrame = frame
else:
break
for i in range(ind + 1, len(cols)):
if len(tmpcols[i]) == 0:
break
frame = tmpcols[i][0]
framelen = frame.Setup.Length / 2
compFramelen = compFrame.Setup.Length / 2
l = max([framelen, compFramelen])
if abs(compFrame.Placement.Base.y - frame.Placement.Base.y) <= l:
group.append(frame)
tmpcols[i].remove(frame)
compFrame = frame
else:
break
if len(group) > 0:
group = sorted(group, key=lambda x: x.Placement.Base.x)
rows.append(group)
return rows, cols
def getCols(objs):
def getRound(num):
return round(num / 100, 0)
xx = set(getRound(obj.Placement.Base.x) for obj in objs)
xx = sorted(xx)
columns = []
for x in xx:
# 1. identificar los objetos de una columna
tmpcol = []
for obj in objs:
if getRound(obj.Placement.Base.x) == x:
tmpcol.append(obj)
tmpcol = sorted(tmpcol, key=lambda obj: getRound(obj.Placement.Base.y), reverse=True)
for obj in tmpcol:
objs.remove(obj)
# 2. dividir los objetos en grupos:
group = []
col = []
for i, f2 in enumerate(tmpcol):
if i > 0:
f1 = group[-1]
d = abs(f1.Placement.Base.y - f2.Placement.Base.y) - \
(f1.Setup.Length.Value + f2.Setup.Length.Value) / 2
if d > 1000:
col.append(group.copy())
group.clear()
group.append(f2)
col.append(group)
columns.append(col)
return columns
# en el caso de que no sean perpendiculares a x:
def getCols_old(sel, tolerance=4000, sort=True):
# TODO: get only frames from de selection
if not sel:
return
if len(sel) == 0:
return
cols = []
while len(sel) > 0:
obj = sel[0]
p = obj.Shape.BoundBox.Center
vec = obj.Shape.SubShapes[1].SubShapes[1].BoundBox.Center - \
obj.Shape.SubShapes[1].SubShapes[0].BoundBox.Center
n = FreeCAD.Vector(vec.y, -vec.x, 0)
# 1. Detectar los objetos que están en una misma columna
col = []
newsel = []
for obj1 in sel:
if obj1.Shape.BoundBox.isCutPlane(p, n): # todo: esto no es del todo correcto. buscar otra manera
col.append(obj1)
else:
newsel.append(obj1)
sel = newsel.copy()
col = sorted(col, key=lambda k: k.Placement.Base.y, reverse=True) # Orden Norte - Sur (Arriba a abajo)
# 2. Detectar y separar los grupos dentro de una misma columna:
group = []
newcol = []
group.append(col[0])
if len(col) > 1:
for ind in range(0, len(col) - 1):
vec1 = FreeCAD.Vector(col[ind].Placement.Base)
vec1.z = 0
vec2 = FreeCAD.Vector(col[ind + 1].Placement.Base)
vec2.z = 0
distance = abs((vec1 - vec2).Length) - (col[ind].Setup.Width.Value + col[ind + 1].Setup.Width.Value) / 2
if distance > tolerance:
newcol.append(group.copy())
group.clear()
group.append(col[ind + 1])
newcol.append(group)
cols.append(newcol)
if sort:
cols = sorted(cols, key=lambda k: k[0][0].Placement.Base.x, reverse=False)
return cols
# -----------------------------------------------------------------------------------------------------------------------
# Convert
# -----------------------------------------------------------------------------------------------------------------------
class _PVPlantConvertTaskPanel:
'''The editmode TaskPanel for Conversions'''
def __init__(self):
self.To = None
# self.form:
self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantPlacementConvert.ui"))
self.form.setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "Trace.svg")))
self.form.buttonTo.clicked.connect(self.addTo)
def addTo(self):
sel = FreeCADGui.Selection.getSelection()
if len(sel) > 0:
self.To = sel[0]
self.form.editTo.setText(self.To.Label)
def accept(self):
sel = FreeCADGui.Selection.getSelection()
if sel == self.To:
return False
if len(sel) > 0 and self.To is not None:
FreeCAD.ActiveDocument.openTransaction("Convert to")
ConvertObjectsTo(sel, self.To)
return True
return False
def getHeadsAndSoil(frame=None):
if frame == None:
return None
import MeshPart as mp
data = {"heads": [],
"soil": []}
poles = frame.Shape.SubShapes[1].SubShapes[0].SubShapes
for pole in poles:
vec = pole.BoundBox.Center
vec.z = pole.BoundBox.ZMax
data["heads"].append(vec)
data["soil"].extend(mp.projectPointsOnMesh(data["heads"],
FreeCAD.ActiveDocument.Terrain.Mesh,
FreeCAD.Vector(0, 0, 1)))
return data
def moveFrameHead(obj, head=0, dist=0):
import math
print(dist)
dir = 1 if head == 0 else -1
base = obj.Placement.Base
dist /= 2
base.z += dist
angles = obj.Placement.Rotation.toEulerAngles("XYZ")
angley = math.degrees(math.asin(dist/(obj.Setup.Length.Value / 2))) * dir
print(angley)
rot = FreeCAD.Rotation(angles[2], angles[1] + angley, angles[0])
obj.Placement = FreeCAD.Placement(base, rot, FreeCAD.Vector(0, 0, 0))
obj.recompute()
# ---------------------------------------------------------------------------------------------------------------------
# function ConvertObjectsTo
#
# ---------------------------------------------------------------------------------------------------------------------
def ConvertObjectsTo(sel, objTo):
if hasattr(objTo, "Proxy"):
isFrame = objTo.Proxy.__class__ is PVPlantRack._Tracker
# isFrame = issubclass(objTo.Proxy.__class__, PVPlantRack._Frame)
isFrame = True
for obj in sel:
if isFrame:
if hasattr(obj, "Proxy"):
if obj.Proxy.__class__ is PVPlantRack._Tracker:
# if issubclass(obj.Proxy.__class__, PVPlantRack._Frame): # 1. Si los dos son Frames
cp = FreeCAD.ActiveDocument.copyObject(objTo, False)
cp.Placement = obj.Placement
cp.CloneOf = objTo
else: # 2. De un objeto no Frame a Frame
place = FreeCAD.Placement() # obj.Placement
place.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1),
90) # TODO: rotar conforme a lados más largos
bb = None
if obj.isDerivedFrom("Part::Feature"):
bb = obj.Shape.BoundBox
elif obj.isDerivedFrom("Mesh::Feature"):
bb = obj.Mesh.BoundBox
place.Base = bb.Center
cp = FreeCAD.ActiveDocument.copyObject(objTo, False)
cp.Placement = place
if isFrame:
cp.CloneOf = objTo
else: # 3. De un objeto a otro objeto (cualesquieran que sean)
place = FreeCAD.Placement() # obj.Placement
bb = None
if obj.isDerivedFrom("Part::Feature"):
bb = obj.Shape.BoundBox
elif obj.isDerivedFrom("Mesh::Feature"):
bb = obj.Mesh.BoundBox
place.Base = bb.Center
cp = FreeCAD.ActiveDocument.copyObject(objTo, False)
cp.Placement = place
if isFrame:
cp.CloneOf = objTo
FreeCAD.ActiveDocument.removeObject(obj.Name)
FreeCAD.activeDocument().recompute()
## Comandos: -----------------------------------------------------------------------------------------------------------
class CommandPVPlantPlacement:
def GetResources(self):
return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "way.svg")),
'Accel': "P,P",
'MenuText': QT_TRANSLATE_NOOP("Placement", "Placement"),
'ToolTip': QT_TRANSLATE_NOOP("Placement", "Crear un campo fotovoltaico")}
def Activated(self):
taskd = _PVPlantPlacementTaskPanel(None)
FreeCADGui.Control.showDialog(taskd)
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
class CommandAdjustToTerrain:
def GetResources(self):
return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "adjust.svg")),
'Accel': "P, A",
'MenuText': QT_TRANSLATE_NOOP("Placement", "Adjust"),
'ToolTip': QT_TRANSLATE_NOOP("Placement", "Adjust object to terrain")}
def Activated(self):
sel = FreeCADGui.Selection.getSelection()
if len(sel) > 0:
# adjustToTerrain(sel)
FreeCADGui.Control.showDialog(adjustToTerrainTaskPanel())
else:
print("No selected object")
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
class CommandConvert:
def GetResources(self):
return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "convert.svg")),
'Accel': "P, C",
'MenuText': QT_TRANSLATE_NOOP("Placement", "Convert"),
'ToolTip': QT_TRANSLATE_NOOP("Placement", "Convertir un objeto en otro")}
def IsActive(self):
return (not FreeCAD.ActiveDocument is None and
not FreeCAD.ActiveDocument.getObject("Site") is None and
not FreeCAD.ActiveDocument.getObject("Terrain") is None and
not FreeCAD.ActiveDocument.getObject("TrackerSetup") is None)
def Activated(self):
taskd = _PVPlantConvertTaskPanel()
FreeCADGui.Control.showDialog(taskd)
'''if FreeCAD.GuiUp:
FreeCADGui.addCommand('PVPlantPlacement', _CommandPVPlantPlacement())
FreeCADGui.addCommand('PVPlantAdjustToTerrain', _CommandAdjustToTerrain())
FreeCADGui.addCommand('PVPlantConvertTo', _CommandConvert())'''