# /********************************************************************** # * * # * 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: '''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() 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())'''