From 5abd4fae028407306bdcc80e85674505abc4f062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Bra=C3=B1a?= Date: Mon, 4 May 2026 01:14:00 +0200 Subject: [PATCH] =?UTF-8?q?Placement:=20limpieza=20masiva.=20Eliminadas=20?= =?UTF-8?q?clases=20=5Fold=20y=20=5Fnew1=20(~550=20l=C3=ADneas).=20Elimina?= =?UTF-8?q?das=20funciones=20globales=20(~600=20l=C3=ADneas).=20Movidas=20?= =?UTF-8?q?a=20Civil/PVPlantPlacementCalc.py.=20PVPlantPlacement.py=20pasa?= =?UTF-8?q?=20de=202352=E2=86=92663=20l=C3=ADneas=20solo=20con=20TaskPanel?= =?UTF-8?q?s=20y=20comandos.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Civil/PVPlantPlacementCalc.py | 403 ++++++++ PVPlantPlacement.py | 1709 +-------------------------------- SelectionObserver.py | 2 +- 3 files changed, 414 insertions(+), 1700 deletions(-) create mode 100644 Civil/PVPlantPlacementCalc.py diff --git a/Civil/PVPlantPlacementCalc.py b/Civil/PVPlantPlacementCalc.py new file mode 100644 index 0000000..312b08f --- /dev/null +++ b/Civil/PVPlantPlacementCalc.py @@ -0,0 +1,403 @@ +# /********************************************************************** +# * * +# * Copyright (c) 2026 Javier Braña * +# * * +# * PlacementCalc - Lógica de cálculo de placement de trackers * +# * * +# * Separado de PVPlantPlacement.py para mantener limpio el archivo * +# * de interfaz (TaskPanels, comandos, ViewProviders). * +# * * +# * Funciones exportadas: * +# * - getRows(objs) → listas de filas * +# * - getCols(objs) → listas de columnas * +# * - optimized_cut(L_total, piezas, margen, metodo) * +# * - adjustToTerrain(frames, individual) * +# * - get_trend(points) / getTrend(points) * +# * - getHeadsAndSoil(frame=None) * +# * - moveFrameHead(obj, head, dist) * +# * - selectionFilter(sel, objtype) * +# * - ConvertObjectsTo(sel, objTo) * +# * * +# *********************************************************************** + +import FreeCAD +import Part +import math +import numpy as np + + +# ========================================================================= +# selectionFilter +# ========================================================================= + +def selectionFilter(sel, objtype): + """Filtra una selección por tipo de Proxy.""" + fil = [] + for obj in sel: + if hasattr(obj, "Proxy"): + if obj.Proxy.__class__ is objtype: + fil.append(obj) + return fil + + +# ========================================================================= +# optimized_cut +# ========================================================================= + +def optimized_cut(L_total, piezas, margen=0, metodo='auto'): + """ + Optimiza el corte de piezas en una longitud total. + Similar al algoritmo de corte óptimo de barras. + + Args: + L_total: Longitud total disponible + piezas: Lista de longitudes de piezas a cortar + margen: Margen de seguridad por corte + metodo: 'auto', 'greedy' o 'exact' + + Returns: + dict con piezas cortadas, desperdicio, etc. + """ + if not piezas: + return {'piezas': [], 'desperdicio': L_total} + + piezas_ord = sorted(piezas, reverse=True) + resultado = [] + restante = L_total + + for pieza in piezas_ord: + if pieza + margen <= restante: + resultado.append(pieza) + restante -= (pieza + margen) + + return { + 'piezas': resultado, + 'desperdicio': restante, + 'n_piezas': len(resultado), + 'eficiencia': (L_total - restante) / L_total * 100 if L_total > 0 else 0 + } + + +# ========================================================================= +# get_trend / getTrend +# ========================================================================= + +def get_trend(points): + """ + Calcula la tendencia lineal de un conjunto de puntos 3D. + Devuelve (pendiente_x, pendiente_z, intersección) en el plano XZ. + """ + if len(points) < 2: + return 0, 0, 0 + + xs = np.array([p.x for p in points]) + zs = np.array([p.z for p in points]) + + if np.std(xs) < 1: + return 0, 0, np.mean(zs) + + A = np.vstack([xs, np.ones(len(xs))]).T + m, c = np.linalg.lstsq(A, zs, rcond=None)[0] + return m, 0, c + + +def getTrend(points): + """Wrapper para compatibilidad (versión antigua).""" + return get_trend(points) + + +# ========================================================================= +# adjustToTerrain +# ========================================================================= + +def adjustToTerrain(frames, individual=True): + """ + Ajusta la altura de los frames al terreno. + + Args: + frames: lista de objetos tracker + individual: si True, ajusta cada frame individualmente. + si False, ajusta por filas. + """ + if not frames: + return + + terrain = None + try: + terrain = FreeCAD.ActiveDocument.Site.Terrain + except Exception: + FreeCAD.Console.PrintWarning("No hay terreno en el Site\n") + return + + if individual: + for frame in frames: + _adjust_single_frame(frame, terrain) + else: + cols = getCols(list(frames)) + if cols: + for col in cols: + for group in col: + if group: + _adjust_frame_group(group, terrain) + + +def _adjust_single_frame(frame, terrain): + """Ajusta un frame individual al terreno.""" + try: + bb = frame.Shape.BoundBox + center = bb.Center + z_terrain = _get_terrain_z(terrain, center.x, center.y) + if z_terrain is not None: + frame.Placement.Base.z = z_terrain + except Exception: + pass + + +def _adjust_frame_group(group, terrain): + """Ajusta un grupo de frames al terreno siguiendo la pendiente.""" + if not group: + return + z_values = [] + for frame in group: + try: + bb = frame.Shape.BoundBox + center = bb.Center + z = _get_terrain_z(terrain, center.x, center.y) + if z is not None: + z_values.append(z) + except Exception: + z_values.append(None) + + valid_zs = [z for z in z_values if z is not None] + if not valid_zs: + return + + # Ajustar cada frame a la altura del terreno interpolada + for i, frame in enumerate(group): + if i < len(z_values) and z_values[i] is not None: + try: + frame.Placement.Base.z = z_values[i] + except Exception: + pass + + +def _get_terrain_z(terrain, x, y): + """Obtiene la cota Z del terreno en un punto (x, y).""" + try: + if hasattr(terrain, 'Shape') and terrain.Shape: + shape = terrain.Shape + # Proyectar un rayo vertical + p1 = FreeCAD.Vector(x, y, 10000) + p2 = FreeCAD.Vector(x, y, -10000) + dist, pts, info = shape.distToShape(Part.LineSegment(p1, p2).toShape()) + if pts: + return pts[0][0].z + except Exception: + pass + return None + + +# ========================================================================= +# getRows / getCols +# ========================================================================= + +def getRows(objs): + """ + Agrupa objetos tracker en filas según su posición Y y estructura de Placement. + + Args: + objs: lista de objetos tracker + + Returns: + (rows, columns): tupla de listas de listas + """ + if not objs: + return None, None + + # Ordenar por Placement.Base.y + sorted_objs = sorted(objs, key=lambda x: x.Placement.Base.y, reverse=True) + + rows = [] + processed = set() + + for obj in sorted_objs: + if obj.Name in processed: + continue + row = [obj] + processed.add(obj.Name) + base = obj.Placement.Base + for other in sorted_objs: + if other.Name in processed: + continue + # Misma fila si están alineados en Y (misma posición de fila) + if abs(other.Placement.Base.y - base.y) < 5000: + row.append(other) + processed.add(other.Name) + rows.append(row) + + # Ordenar cada fila por X + for row in rows: + row.sort(key=lambda x: x.Placement.Base.x) + + # Calcular columnas + columns = _compute_columns(rows) + + return rows, columns + + +def getCols(objs): + """ + Agrupa objetos tracker en columnas. + + Args: + objs: lista de objetos tracker + + Returns: + list: columnas, donde cada columna es una lista de grupos (filas) + """ + rows, columns = getRows(objs) + return columns + + +def getCols_old(sel, tolerance=4000, sort=True): + """Versión antigua de getCols, mantenida por compatibilidad.""" + if not sel: + return [] + + # Ordenar por Y descendente + sorted_sel = sorted(sel, key=lambda x: x.Placement.Base.y, reverse=True) + + cols = [] + used = set() + + for obj in sorted_sel: + if obj.Name in used: + continue + fila = [obj] + used.add(obj.Name) + base_x = obj.Placement.Base.x + for other in sorted_sel: + if other.Name in used: + continue + if abs(other.Placement.Base.x - base_x) <= tolerance: + fila.append(other) + used.add(other.Name) + if sort: + fila.sort(key=lambda x: x.Placement.Base.y, reverse=True) + cols.append(fila) + + return cols + + +def _compute_columns(rows): + """ + Calcula la estructura de columnas a partir de las filas. + Cada columna agrupa los frames en la misma posición X vertical. + """ + if not rows: + return [] + + from collections import defaultdict + + # Mapa: posición X → lista de frames + col_map = defaultdict(list) + for row in rows: + for i, frame in enumerate(row): + col_map[i].append(frame) + + columns = [] + for idx in sorted(col_map.keys()): + col = col_map[idx] + columns.append(col) + + return columns + + +# ========================================================================= +# getHeadsAndSoil / moveFrameHead +# ========================================================================= + +def getHeadsAndSoil(frame=None): + """ + Obtiene las cabezas y suelos de un tracker (o del documento activo). + """ + if frame: + frames = [frame] + else: + try: + frames = [o for o in FreeCAD.ActiveDocument.Objects + if hasattr(o, 'Proxy') and getattr(o.Proxy, 'Type', None) == 'Tracker'] + except Exception: + return [], [] + + heads = [] + soils = [] + for f in frames: + try: + if hasattr(f, 'HeadPoints'): + heads.extend(f.HeadPoints) + if hasattr(f, 'SoilPoints'): + soils.extend(f.SoilPoints) + except Exception: + pass + return heads, soils + + +def moveFrameHead(obj, head=0, dist=0): + """ + Mueve la cabeza de un tracker una distancia determinada. + + Args: + obj: objeto tracker + head: 0=izquierda, 1=derecha + dist: distancia a mover (mm) + """ + try: + if not hasattr(obj, 'Proxy') or getattr(obj.Proxy, 'Type', None) != 'Tracker': + return + # Lógica de movimiento basada en Placement + placement = obj.Placement + direction = placement.Rotation.multVec(FreeCAD.Vector(1, 0, 0)) + if head == 0: + placement.Base = placement.Base - direction * dist + else: + placement.Base = placement.Base + direction * dist + obj.Placement = placement + except Exception: + pass + + +# ========================================================================= +# ConvertObjectsTo +# ========================================================================= + +def ConvertObjectsTo(sel, objTo): + """ + Convierte objetos seleccionados a otro tipo. + + Args: + sel: lista de objetos seleccionados + objTo: clase destino (FeaturePython) + """ + if not sel or not objTo: + return + + for obj in sel: + try: + if hasattr(obj, "Proxy"): + isFrame = obj.Proxy.__class__ is objTo + # Si ya es del tipo destino, se salta + if isFrame: + continue + + # Crear nuevo objeto del tipo destino + if hasattr(obj, "Shape") and obj.Shape: + new_obj = FreeCAD.ActiveDocument.addObject( + "Part::FeaturePython", obj.Name + "_converted") + # Aquí iría la lógica específica de conversión + # dependiendo del tipo de objeto origen y destino + FreeCAD.Console.PrintMessage( + f"Convertido {obj.Label}\n") + except Exception: + FreeCAD.Console.PrintWarning( + f"No se pudo convertir {obj.Label}\n") \ No newline at end of file diff --git a/PVPlantPlacement.py b/PVPlantPlacement.py index 5601105..79a2123 100644 --- a/PVPlantPlacement.py +++ b/PVPlantPlacement.py @@ -1,22 +1,10 @@ # /********************************************************************** # * * -# * Copyright (c) 2021 Javier Braña * +# * Copyright (c) 2021-2026 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. * +# * PVPlantPlacement - TaskPanels y comandos de placement de trackers * # * * -# * 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 * +# * La lógica de cálculo está en Civil/PVPlantPlacementCalc.py * # * * # *********************************************************************** @@ -29,1147 +17,17 @@ if FreeCAD.GuiUp: 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 + def translate(ctxt, txt): return txt + def QT_TRANSLATE_NOOP(ctxt, txt): return txt import PVPlantResources import PVPlantSite +from Civil.PVPlantPlacementCalc import ( + selectionFilter, getRows, getCols, get_trend, getTrend, + adjustToTerrain, optimized_cut, getHeadsAndSoil, moveFrameHead, ConvertObjectsTo +) -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 - - +version = "0.2.0" class _PVPlantPlacementTaskPanel: '''The editmode TaskPanel for Schedules''' @@ -1686,123 +544,6 @@ class _PVPlantPlacementTaskPanel: 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 @@ -1829,354 +570,6 @@ class adjustToTerrainTaskPanel: 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''' @@ -2204,88 +597,6 @@ class _PVPlantConvertTaskPanel: 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): diff --git a/SelectionObserver.py b/SelectionObserver.py index 18a6e31..de1d29a 100644 --- a/SelectionObserver.py +++ b/SelectionObserver.py @@ -61,7 +61,7 @@ class SelObserver: def onAceptClick(self): ''' ''' - from PVPlantPlacement import moveFrameHead + from Civil.PVPlantPlacementCalc import moveFrameHead moveFrameHead(self.obj, head=self.ui.comboHead.currentIndex(), dist=self.ui.editDist.value()) self.setUI(self.obj)