Files
PVPlant/PVPlantPlacement.py

1141 lines
46 KiB
Python
Raw Permalink Normal View History

2025-01-28 00:04:13 +01:00
# /**********************************************************************
# * *
# * 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.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
2025-03-28 19:40:11 +06:00
version = "0.1.0"
2025-01-28 00:04:13 +01:00
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:
'''The editmode TaskPanel for Schedules'''
def __init__(self, obj=None):
self.Terrain = PVPlantSite.get().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.form.buttonPVArea.clicked.connect(self.addPVArea)
self.form.buttonAddFrame.clicked.connect(self.addFrame)
self.form.buttonRemoveFrame.clicked.connect(self.removeFrame)
def addPVArea(self):
sel = FreeCADGui.Selection.getSelection()
if len(sel) > 0:
self.PVArea = sel[0]
self.form.editPVArea.setText(self.PVArea.Label)
def addFrame(self):
from Mechanical.Frame import PVPlantFrame
selection = FreeCADGui.Selection.getSelection()
self.FrameSetup = selectionFilter(selection, PVPlantFrame.TrackerSetup)
if len(selection) > 0:
items = []
for x in range(self.form.listFrameSetups.count()):
items.append(self.form.listFrameSetups.item(x).text())
if not (selection[0].Name in items):
self.form.listFrameSetups.addItem(selection[0].Name)
def removeFrame(self):
''' remove select item from list '''
self.form.listFrameSetups.takeItem(self.form.listFrameSetups.currentRow())
def 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)
placements = dataframe["placement"].tolist()
types = dataframe["type"].tolist()
frames = []
for idx in range(len(placements)):
newrack = PVPlantFrame.makeTracker(setup=types[idx])
newrack.Label = "Tracker"
2025-03-28 19:40:11 +06:00
newrack.Visibility = False
2025-01-28 00:04:13 +01:00
newrack.Placement = placements[idx]
MechanicalGroup.addObject(newrack)
frames.append(newrack)
if self.PVArea.Name.startswith("FrameArea"):
self.PVArea.Frames = frames
# TODO: else
def getProjected(self, shape):
""" returns projected edges from a shape and a direction """
if shape.BoundBox.ZLength == 0:
edges = shape.Edges
return Part.Face(Part.Wire(edges))
else:
from Utils import PVPlantUtils as utils
wire = utils.simplifyWire(utils.getProjected(shape))
2025-03-28 19:40:11 +06:00
if wire.isClosed():
wire = wire.removeSplitter()
2025-01-28 00:04:13 +01:00
return Part.Face(wire)
def calculateWorkingArea(self):
self.Area = self.getProjected(self.PVArea.Shape)
2025-04-14 10:05:32 +06:00
tmp = FreeCAD.ActiveDocument.findObjects(Name="ExclusionArea")
2025-01-28 00:04:13 +01:00
if len(tmp):
ProhibitedAreas = list()
for obj in tmp:
face = self.getProjected(obj.Base.Shape)
if face.isValid():
ProhibitedAreas.append(face)
self.Area = self.Area.cut(ProhibitedAreas)
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 ----------------------------------------------------------------------------------
2025-03-28 19:40:11 +06:00
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)
2025-01-28 00:04:13 +01:00
def adjustToTerrain(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()
2025-04-14 10:05:32 +06:00
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.
2025-01-28 00:04:13 +01:00
tmp = terrain.makeParallelProjection(line, FreeCAD.Vector(0, 0, 1))
2025-04-14 10:05:32 +06:00
profilepoints = [ver.Point for ver in tmp.Vertexes]'''
2025-01-28 00:04:13 +01:00
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:
2025-03-28 19:40:11 +06:00
from scipy.ndimage import label as sclabel
2025-01-28 00:04:13 +01:00
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
2025-04-14 10:05:32 +06:00
"""def placeonregion_old(frames): # old
2025-01-28 00:04:13 +01:00
for colnum, col in enumerate(frames):
groups = list()
groups.append([col[0]])
for i in range(1, len(col)):
group = groups[-1]
long = (col[i].sub(group[-1])).Length
long -= width
if long <= dist:
group.append(col[i])
else:
groups.append([col[i]])
for group in groups:
points = list()
points.append(group[0].sub(vec1))
for ind in range(0, len(group) - 1):
points.append((group[ind].sub(vec1) + group[ind + 1].add(vec1)) / 2)
points.append(group[-1].add(vec1))
points3D = list()
'''
# v0
for ind in range(len(points) - 1):
line = Part.LineSegment(points[ind], points[ind + 1])
tmp = terrain.makeParallelProjection(line.toShape(), FreeCAD.Vector(0, 0, 1))
if len(tmp.Vertexes) > 0:
if ind == 0:
points3D.append(tmp.Vertexes[0].Point)
points3D.append(tmp.Vertexes[-1].Point)
'''
# V1
if type == 0: # Mesh:
import MeshPart as mp
for point in points:
point3D = mp.projectPointsOnMesh([point, ], terrain, FreeCAD.Vector(0, 0, 1))
if len(point3D) > 0:
points3D.append(point3D[0])
else: # Shape:
line = Part.LineSegment(points[0], points[-1])
tmp = terrain.makeParallelProjection(line.toShape(), FreeCAD.Vector(0, 0, 1))
if len(tmp.Vertexes) > 0:
tmppoints = [ver.Point for ver in tmp.Vertexes]
if mode == 1: # mode = normal
for point in points:
'''# OPTION 1:
line = Part.Line(point, point + FreeCAD.Vector(0, 0, 10))
for i in range(len(tmppoints) - 1):
seg = Part.LineSegment(tmppoints[i], tmppoints[i + 1])
inter = line.intersect(seg)
print(inter)
if len(inter) > 0:
points3D.append(FreeCAD.Vector(inter[0].X, inter[0].Y, inter[0].Z))
'''
# OPTION 2:
plane = Part.Plane(point, self.Dir)
for i in range(len(tmppoints) - 1):
seg = Part.LineSegment(tmppoints[i], tmppoints[i + 1])
inter = plane.intersect(seg)
if len(inter) > 0:
if len(inter[0]):
inter = inter[0]
points3D.append(FreeCAD.Vector(inter[0].X, inter[0].Y, inter[0].Z))
break
else: # TODO: mode = Trend
# TODO: check:
from scipy import stats
xx = list()
yy = list()
zz = list()
for pts in tmppoints:
xx.append(pts.x)
yy.append(pts.y)
zz.append(pts.z)
slope, intercept, r, p, std_err = stats.linregress(yy, zz)
def myfunc(x):
return slope * x + intercept
x = list()
x.append(yy[0])
x.append(yy[-1])
newzz = list(map(myfunc, x))
points3D.append(FreeCAD.Vector(xx[0], yy[0], newzz[0]))
points3D.append(FreeCAD.Vector(xx[-1], yy[-1], newzz[1]))
for ind in range(0, len(points3D) - 1):
pl = FreeCAD.Placement()
vec = points3D[ind] - points3D[ind + 1]
pl.Base = FreeCAD.Vector(group[ind])
p = (points3D[ind] + points3D[ind + 1]) / 2
pl.Base.z = p.z
rot = FreeCAD.Rotation(FreeCAD.Vector(-1, 0, 0), vec)
pl.Rotation = FreeCAD.Rotation(rot.toEuler()[0], rot.toEuler()[1], 0)
placements.append(pl)
2025-04-14 10:05:32 +06:00
return placements"""
2025-01-28 00:04:13 +01:00
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):
2025-03-28 19:40:11 +06:00
import FreeCAD
2025-01-28 00:04:13 +01:00
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
2025-04-14 10:05:32 +06:00
print("/n/n")
print(cols)
2025-01-28 00:04:13 +01:00
return self.adjustToTerrain(cols)
def calculateNonAlignedArray(self):
gap_col = FreeCAD.Units.Quantity(self.form.editGapCols.text()).Value
gap_row = FreeCAD.Units.Quantity(self.form.editGapRows.text()).Value + max(self.Rack.Shape.BoundBox.XLength,
self.Rack.Shape.BoundBox.YLength)
offset_x = FreeCAD.Units.Quantity(self.form.editOffsetHorizontal.text()).Value
offset_y = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value
Area = self.calculateWorkingArea()
rec = Part.makePlane(self.Rack.Shape.BoundBox.YLength, self.Rack.Shape.BoundBox.XLength)
# TODO: revisar todo esto: -----------------------------------------------------------------
sel = FreeCADGui.Selection.getSelectionEx()[0]
refh = None
refv = None
if len(sel.SubObjects) == 0:
refh = refv = Area.Edges[0]
if len(sel.SubObjects) == 1:
refh = refv = sel.SubObjects[0]
if len(sel.SubObjects) == 2:
if sel.SubObjects[0].BoundBox.XLength > sel.SubObjects[1].BoundBox.XLength:
refh = sel.SubObjects[0]
else:
refh = sel.SubObjects[1]
if sel.SubObjects[0].BoundBox.YLength > sel.SubObjects[1].BoundBox.YLength:
refv = sel.SubObjects[0]
else:
refv = sel.SubObjects[1]
steps = int((refv.BoundBox.XMax - Area.BoundBox.XMin + offset_x) / gap_col)
startx = refv.BoundBox.XMax + offset_x - gap_col * steps
# todo end ----------------------------------------------------------------------------------
start = FreeCAD.Vector(startx, 0.0, 0.0)
pointsx = np.arange(start.x, Area.BoundBox.XMax, gap_col)
if self.form.groupCorridor.isChecked():
if (self.form.editColCount.value() > 0):
xlen = len(pointsx)
count = self.form.editColCount.value()
val = FreeCAD.Units.Quantity(self.form.editColGap.text()).Value - (
gap_col - min(self.Rack.Shape.BoundBox.XLength, self.Rack.Shape.BoundBox.YLength))
while count <= xlen:
for i, point in enumerate(pointsx):
if i >= count:
pointsx[i] += val
count += self.form.editColCount.value()
pl = []
for point in pointsx:
p1 = FreeCAD.Vector(point, Area.BoundBox.YMax, 0.0)
p2 = FreeCAD.Vector(point, Area.BoundBox.YMin, 0.0)
line = Part.makePolygon([p1, p2])
inter = Area.section([line])
pts = [ver.Point for ver in inter.Vertexes] # todo: sort points
for i in range(0, len(pts), 2):
line = Part.LineSegment(pts[i], pts[i + 1])
if line.length() >= rec.BoundBox.YLength:
y1 = pts[i].y - rec.BoundBox.YLength
cp = rec.copy()
cp.Placement.Base = FreeCAD.Vector(pts[i].x - rec.BoundBox.XLength / 2, y1, 0.0)
inter = cp.cut([Area])
y1 = min([ver.Point.y for ver in inter.Vertexes])
pointsy = np.arange(y1, pts[i + 1].y, -gap_row)
for point in pointsy:
cp = rec.copy()
cp.Placement.Base = FreeCAD.Vector(pts[i].x - rec.BoundBox.XLength / 2, point, 0.0)
cut = cp.cut([Area], 0)
if len(cut.Vertexes) == 0:
Part.show(cp)
pl.append(point)
return pl
def accept(self):
from datetime import datetime
starttime = datetime.now()
2025-03-28 19:40:11 +06:00
params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Document")
auto_save_enabled = params.GetBool("AutoSaveEnabled")
params.SetBool("AutoSaveEnabled", False)
FreeCAD.ActiveDocument.RecomputesFrozen = True
2025-01-28 00:04:13 +01:00
items = []
for x in range(self.form.listFrameSetups.count()):
items.append(FreeCAD.ActiveDocument.getObject(self.form.listFrameSetups.item(x).text()))
tmpframes = list()
for frame in sorted(items, key=lambda rack: rack.Length, reverse=True):
found = False
for tmp in tmpframes:
if tmp.Length == frame.Length:
found = True
break
if not found:
tmpframes.append(frame)
self.FrameSetups = tmpframes.copy()
longerFrame = self.FrameSetups[0]
self.gap_col = FreeCAD.Units.Quantity(self.form.editGapCols.text()).Value
self.gap_row = FreeCAD.Units.Quantity(self.form.editGapRows.text()).Value + longerFrame.Length.Value
self.offsetX = FreeCAD.Units.Quantity(self.form.editOffsetHorizontal.text()).Value
self.offsetY = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value
FreeCAD.ActiveDocument.openTransaction("Create Placement")
2025-04-14 10:05:32 +06:00
# 1. Calculate working area:
2025-01-28 00:04:13 +01:00
self.calculateWorkingArea()
2025-04-14 10:05:32 +06:00
# 2. Calculate aligned array:
2025-01-28 00:04:13 +01:00
if self.form.cbAlignFrames.isChecked():
dataframe = self.calculateAlignedArray()
else:
dataframe = self.calculateNonAlignedArray()
2025-04-14 10:05:32 +06:00
# 3. Adjust to terrain:
self.createFrameFromPoints(dataframe)
FreeCAD.ActiveDocument.commitTransaction()
2025-01-28 00:04:13 +01:00
FreeCAD.ActiveDocument.RecomputesFrozen = False
2025-03-28 19:40:11 +06:00
params.SetBool("AutoSaveEnabled", auto_save_enabled)
2025-01-28 00:04:13 +01:00
total_time = datetime.now() - starttime
print(" -- Tiempo tardado:", total_time)
FreeCADGui.Control.closeDialog()
FreeCAD.ActiveDocument.recompute()
2025-03-28 19:40:11 +06:00
#return True
2025-01-28 00:04:13 +01:00
# ----------------------------------------------------------------------------------------------------------------------
# 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: -----------------------------------------------------------------------------------------------------------
2025-03-28 19:40:11 +06:00
class CommandPVPlantPlacement:
2025-01-28 00:04:13 +01:00
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
2025-03-28 19:40:11 +06:00
class CommandAdjustToTerrain:
2025-01-28 00:04:13 +01:00
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
2025-03-28 19:40:11 +06:00
class CommandConvert:
2025-01-28 00:04:13 +01:00
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)
2025-03-28 19:40:11 +06:00
'''if FreeCAD.GuiUp:
2025-01-28 00:04:13 +01:00
FreeCADGui.addCommand('PVPlantPlacement', _CommandPVPlantPlacement())
FreeCADGui.addCommand('PVPlantAdjustToTerrain', _CommandAdjustToTerrain())
2025-03-28 19:40:11 +06:00
FreeCADGui.addCommand('PVPlantConvertTo', _CommandConvert())'''