# /********************************************************************** # * * # * Copyright (c) 2021 Javier Braña * # * * # * This program is free software; you can redistribute it and/or modify* # * it under the terms of the GNU Lesser General Public License (LGPL) * # * as published by the Free Software Foundation; either version 2 of * # * the License, or (at your option) any later version. * # * for detail see the LICENCE text file. * # * * # * This program is distributed in the hope that it will be useful, * # * but WITHOUT ANY WARRANTY; without even the implied warranty of * # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * # * GNU Library General Public License for more details. * # * * # * You should have received a copy of the GNU Library General Public * # * License along with this program; if not, write to the Free Software * # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307* # * USA * # * * # *********************************************************************** import FreeCAD import 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 PySide import QtCore, QtGui from PySide.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 = {} self._isinside_cache = {} # LRU: (frame_name, x, y) -> bool self._area_polygon = None # Caché shapely del área # 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): """Gestión optimizada de grupos de frames""" doc = FreeCAD.ActiveDocument main_group = doc.getObject("Frames") if not main_group: main_group = doc.addObject("App::DocumentObjectGroup", "Frames") main_group.Label = "Frames" mg = doc.getObject('MechanicalGroup') if mg and main_group not in mg.Group: mg.addObject(main_group) if self.form.cbSubfolders.isChecked() and self.PVArea: sn = f"Frames-{self.PVArea.Label}" sg = next((o for o in main_group.Group if o.Name == sn), None) if not sg: sg = doc.addObject("App::DocumentObjectGroup", sn) sg.Label = sn main_group.addObject(sg) return sg 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 caches when area changes self._terrain_interpolator = None self._area_polygon = None self._isinside_cache.clear() 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 _get_area_polygon(self): """Convierte self.Area a shapely Polygon para comprobaciones rápidas""" if self._area_polygon is None and self.Area: from shapely.geometry import Polygon verts = self.Area.Vertexes if len(verts) >= 3: self._area_polygon = Polygon([(v.x, v.y) for v in verts]) return self._area_polygon def isInside(self, frame, point): """ Comprueba si un frame cabe en el área en un punto dado. Usa shapely para la comprobación 2D (mucho más rápido que Part.cut). """ # Caché LRU: mismo frame + misma posición key = (frame.Name, round(point.x, 0), round(point.y, 0)) if key in self._isinside_cache: return self._isinside_cache[key] # Prefiltro rápido por BoundBox fw, fl = frame.Width.Value / 2, frame.Length.Value / 2 if (point.x - fw < self.Area.BoundBox.XMin or point.x + fw > self.Area.BoundBox.XMax or point.y - fl < self.Area.BoundBox.YMin or point.y + fl > self.Area.BoundBox.YMax): self._isinside_cache[key] = False return False # Comprobación precisa con shapely ap = self._get_area_polygon() if ap is not None: from shapely.geometry import box fp = box(point.x - fw, point.y - fl, point.x + fw, point.y + fl) result = ap.contains(fp) self._isinside_cache[key] = result return result # Fallback OCC (si shapely falla) try: frame_footprint = self._get_frame_footprint(frame) frame_footprint.Placement.Base = point cut = frame_footprint.cut([self.Area]) result = len(cut.Vertexes) == 0 self._isinside_cache[key] = result return result except Part.OCCError: self._isinside_cache[key] = False return False def getAligments(self): """ Calcula las alineaciones X (columnas) y opcionalmente Y (filas) en función de las referencias seleccionadas. Retorna (x_range, y_range). y_range vacío si no hay referencia vertical. """ sel = FreeCADGui.Selection.getSelectionEx() if not sel or not sel[0].SubObjects: return np.array([], dtype=np.float64), np.array([], dtype=np.float64) sub_objects = sel[0].SubObjects if len(sub_objects) == 1: # Una sola referencia: usar BoundBox completo bb = sub_objects[0].BoundBox area_bb = self.Area.BoundBox n_cols = max(1, int((area_bb.XMax - area_bb.XMin) / self.gap_col)) n_rows = max(1, int((area_bb.YMax - area_bb.YMin) / self.gap_row)) x_range = np.linspace(area_bb.XMin + self.offsetX, area_bb.XMax, n_cols, dtype=np.float64) y_range = np.linspace(area_bb.YMax - self.offsetY - self.gap_row, area_bb.YMin, n_rows, dtype=np.float64) else: refh = max(sub_objects[:2], key=lambda x: x.BoundBox.XLength) refv = max(sub_objects[:2], key=lambda x: x.BoundBox.YLength) # Alinear grid con referencias area_xmin, area_xmax = self.Area.BoundBox.XMin, self.Area.BoundBox.XMax area_ymin, area_ymax = self.Area.BoundBox.YMin, self.Area.BoundBox.YMax n_cols = max(1, int((area_xmax - area_xmin) / self.gap_col)) n_rows = max(1, int((area_ymax - area_ymin) / self.gap_row)) x_range = np.linspace( refv.BoundBox.XMin + self.offsetX, min(refv.BoundBox.XMax + self.offsetX, area_xmax), n_cols, dtype=np.float64 ) y_range = np.linspace( refh.BoundBox.YMax - self.offsetY, max(refh.BoundBox.YMin - self.offsetY, area_ymin), n_rows, dtype=np.float64 ) if n_rows > 1 else np.array([refh.BoundBox.YMin], dtype=np.float64) # Pre-filtrar: eliminar puntos claramente fuera del BoundBox del área x_range = x_range[(x_range >= self.Area.BoundBox.XMin) & (x_range <= self.Area.BoundBox.XMax)] if len(y_range) > 0: y_range = y_range[(y_range >= self.Area.BoundBox.YMin) & (y_range <= self.Area.BoundBox.YMax)] return x_range, y_range def calculateAlignedArray(self): """ Coloca frames en grid alineado (filas y columnas). Llama al motor unificado _calculate_placement. """ return self._calculate_placement(mode='aligned') def calculateNonAlignedArray(self): """ Coloca frames adaptados al contorno del área (solo columnas). Llama al motor unificado _calculate_placement. """ return self._calculate_placement(mode='non_aligned') def _calculate_placement(self, mode='non_aligned'): """ Motor de posicionamiento unificado para aligned y non_aligned. aligned: grid Y fijo + isInside (rápido en áreas rectangulares, usa caché) non_aligned: intersección área-línea (preciso en bordes irregulares) """ pointsx, pointsy = self.getAligments() if len(pointsx) == 0: FreeCAD.Console.PrintWarning("No X alignments found.\n") return pd.DataFrame() # Pre-calcular footprints una sola vez footprints = [] for frame in self.FrameSetups: footprint = self._get_frame_footprint(frame) footprints.append((frame, footprint)) if not footprints: return pd.DataFrame() min_h = min(ftp[0].Width.Value for ftp in footprints) corridor_enabled = self.form.groupCorridor.isChecked() corridor_count = 0 corridor_offset = 0 ref_width = footprints[0][0].Width.Value corridor_val = FreeCAD.Units.Quantity( self.form.editColGap.text()).Value - (self.gap_col - ref_width) area_ymax = self.Area.BoundBox.YMax area_ymin = self.Area.BoundBox.YMin ref_frame = footprints[0][0] ref_len = ref_frame.Length.Value n_cols = len(pointsx) cols = [None] * n_cols # Procesar por lotes para permitir interrupción con barra de progreso from PySide.QtCore import QCoreApplication for idx, x in enumerate(pointsx): col = [] cx = x + corridor_offset # Actualizar barra de progreso cada 20 columnas if idx % 20 == 0 and hasattr(self.form, 'progressBar'): self.form.progressBar.setValue(int(100 * idx / n_cols)) QCoreApplication.processEvents() if mode == 'aligned' and len(pointsy) > 0: half_len = ref_len / 2 for y in pointsy: py = y - half_len # Vector creado solo si pasa el BoundBox check (ya lo hace isInside internamente) tp = FreeCAD.Vector(cx, py, 0.0) placed = False if self.isInside(ref_frame, tp): col.append([ref_frame, tp]) placed = True else: # frames alternativos: probar con offsets for fi in range(1, len(footprints)): fr = footprints[fi][0] ld = (ref_len - fr.Length.Value) / 2 for yoff in (ld, -ld): if self.isInside(fr, FreeCAD.Vector(tp.x, tp.y + yoff, 0.0)): col.append([fr, FreeCAD.Vector(tp.x, tp.y + yoff, 0.0)]) placed = True break if placed: break if not placed: col.append(0) else: # Non-aligned: intersección de línea vertical con el área line = Part.LineSegment( FreeCAD.Vector(cx, area_ymax, 0.0), FreeCAD.Vector(cx, area_ymin, 0.0) ).toShape() 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, 1 + (len(pts) > 2)): top, bot = pts[i], pts[i + 1] if top.y - bot.y > min_h: self._place_frames_in_segment(col, footprints, cx, top, bot) except Exception as e: FreeCAD.Console.PrintWarning(f"Segment error: {e}\n") # Corredores if corridor_enabled and col: corridor_count += 1 if corridor_count >= self.form.editColCount.value(): corridor_offset += corridor_val corridor_count = 0 cols[idx] = 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 try: 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 ] self.FrameSetups = list({f.Length.Value: f for f in items}.values()) self.FrameSetups.sort(key=lambda x: x.Length.Value, 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") self.calculateWorkingArea() if self.form.cbAlignFrames.isChecked(): dataframe = self.calculateAlignedArray() else: dataframe = self.calculateNonAlignedArray() if not dataframe.empty: self.createFrameFromPoints(dataframe) import Electrical.group as egroup import importlib importlib.reload(egroup) egroup.groupTrackersToTransformers(5000000, self.gap_row) FreeCAD.ActiveDocument.commitTransaction() finally: FreeCAD.ActiveDocument.RecomputesFrozen = False params.SetBool("AutoSaveEnabled", auto_save_enabled) elapsed = datetime.now() - starttime FreeCAD.Console.PrintMessage(f"Placement: {elapsed}\n") 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())'''