From f4d43bedd018c2e3f7b05708d09b7d8a8f3c4d87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Bra=C3=B1a?= Date: Sun, 3 May 2026 20:25:40 +0200 Subject: [PATCH] Placement: getAligments con linspace, _calculate_placement progreso, accept simplificado, _get_or_create optimizado --- PVPlantPlacement.py | 141 ++++++++++++++++++++++++++------------------ 1 file changed, 84 insertions(+), 57 deletions(-) diff --git a/PVPlantPlacement.py b/PVPlantPlacement.py index 011d92f..5601105 100644 --- a/PVPlantPlacement.py +++ b/PVPlantPlacement.py @@ -1215,28 +1215,25 @@ class _PVPlantPlacementTaskPanel: self.form.editInnerSpacing.setText(f"{self.form.editGapCols.value() - self.maxWidth / 1000} m") def _get_or_create_frame_group(self): - """Optimized group creation and management""" + """Gestión optimizada de grupos de frames""" doc = FreeCAD.ActiveDocument - - # Get or create main group - main_group = doc.getObject("Frames") or doc.addObject("App::DocumentObjectGroup", "Frames") - if not main_group.Label == "Frames": + main_group = doc.getObject("Frames") + if not main_group: + main_group = doc.addObject("App::DocumentObjectGroup", "Frames") main_group.Label = "Frames" - # Add to MechanicalGroup if exists - if not hasattr(doc, 'MechanicalGroup') and hasattr(doc, 'getObject') and doc.getObject('MechanicalGroup'): - doc.MechanicalGroup.addObject(main_group) + mg = doc.getObject('MechanicalGroup') + if mg and main_group not in mg.Group: + mg.addObject(main_group) - # Handle subfolder if self.form.cbSubfolders.isChecked() and self.PVArea: - subgroup_name = f"Frames-{self.PVArea.Label}" - subgroup = next((obj for obj in main_group.Group if obj.Name == subgroup_name), None) - if not subgroup: - subgroup = doc.addObject("App::DocumentObjectGroup", subgroup_name) - subgroup.Label = subgroup_name - main_group.addObject(subgroup) - return subgroup - + sn = f"Frames-{self.PVArea.Label}" + sg = next((o for o in main_group.Group if o.Name == sn), None) + if not sg: + sg = doc.addObject("App::DocumentObjectGroup", sn) + sg.Label = sn + main_group.addObject(sg) + return sg return main_group def createFrameFromPoints(self, dataframe): @@ -1479,31 +1476,50 @@ class _PVPlantPlacementTaskPanel: return False def getAligments(self): - """Optimized alignment calculation""" + """ + Calcula las alineaciones X (columnas) y opcionalmente Y (filas) + en función de las referencias seleccionadas. + Retorna (x_range, y_range). y_range vacío si no hay referencia vertical. + """ sel = FreeCADGui.Selection.getSelectionEx() if not sel or not sel[0].SubObjects: - return np.array([]), np.array([]) + return np.array([], dtype=np.float64), np.array([], dtype=np.float64) sub_objects = sel[0].SubObjects if len(sub_objects) == 1: - refh = refv = sub_objects[0] + # Una sola referencia: usar BoundBox completo + bb = sub_objects[0].BoundBox + area_bb = self.Area.BoundBox + n_cols = max(1, int((area_bb.XMax - area_bb.XMin) / self.gap_col)) + n_rows = max(1, int((area_bb.YMax - area_bb.YMin) / self.gap_row)) + x_range = np.linspace(area_bb.XMin + self.offsetX, area_bb.XMax, n_cols, dtype=np.float64) + y_range = np.linspace(area_bb.YMax - self.offsetY - self.gap_row, area_bb.YMin, n_rows, dtype=np.float64) else: - # Choose references based on bounding box dimensions refh = max(sub_objects[:2], key=lambda x: x.BoundBox.XLength) refv = max(sub_objects[:2], key=lambda x: x.BoundBox.YLength) - # Calculate ranges efficiently - startx = refv.BoundBox.XMin + self.offsetX - self.gap_col * int( - (refv.BoundBox.XMax - self.Area.BoundBox.XMin + self.offsetX) / self.gap_col - ) + # Alinear grid con referencias + area_xmin, area_xmax = self.Area.BoundBox.XMin, self.Area.BoundBox.XMax + area_ymin, area_ymax = self.Area.BoundBox.YMin, self.Area.BoundBox.YMax - starty = refh.BoundBox.YMin + self.offsetY + self.gap_row * int( - (refh.BoundBox.YMin - self.Area.BoundBox.YMax + self.offsetY) / self.gap_row - ) + n_cols = max(1, int((area_xmax - area_xmin) / self.gap_col)) + n_rows = max(1, int((area_ymax - area_ymin) / self.gap_row)) + x_range = np.linspace( + refv.BoundBox.XMin + self.offsetX, + min(refv.BoundBox.XMax + self.offsetX, area_xmax), + n_cols, dtype=np.float64 + ) + y_range = np.linspace( + refh.BoundBox.YMax - self.offsetY, + max(refh.BoundBox.YMin - self.offsetY, area_ymin), + n_rows, dtype=np.float64 + ) if n_rows > 1 else np.array([refh.BoundBox.YMin], dtype=np.float64) - x_range = np.arange(startx, self.Area.BoundBox.XMax, self.gap_col, dtype=np.float64) - y_range = np.arange(starty, self.Area.BoundBox.YMin, -self.gap_row, dtype=np.float64) + # Pre-filtrar: eliminar puntos claramente fuera del BoundBox del área + x_range = x_range[(x_range >= self.Area.BoundBox.XMin) & (x_range <= self.Area.BoundBox.XMax)] + if len(y_range) > 0: + y_range = y_range[(y_range >= self.Area.BoundBox.YMin) & (y_range <= self.Area.BoundBox.YMax)] return x_range, y_range @@ -1525,7 +1541,7 @@ class _PVPlantPlacementTaskPanel: """ Motor de posicionamiento unificado para aligned y non_aligned. - aligned: grid Y fijo + isInside (rápido en áreas rectangulares) + aligned: grid Y fijo + isInside (rápido en áreas rectangulares, usa caché) non_aligned: intersección área-línea (preciso en bordes irregulares) """ pointsx, pointsy = self.getAligments() @@ -1539,6 +1555,9 @@ class _PVPlantPlacementTaskPanel: footprint = self._get_frame_footprint(frame) footprints.append((frame, footprint)) + if not footprints: + return pd.DataFrame() + min_h = min(ftp[0].Width.Value for ftp in footprints) corridor_enabled = self.form.groupCorridor.isChecked() corridor_count = 0 @@ -1548,34 +1567,49 @@ class _PVPlantPlacementTaskPanel: self.form.editColGap.text()).Value - (self.gap_col - ref_width) area_ymax = self.Area.BoundBox.YMax area_ymin = self.Area.BoundBox.YMin + ref_frame = footprints[0][0] + ref_len = ref_frame.Length.Value - cols = [] - for x in pointsx: + n_cols = len(pointsx) + cols = [None] * n_cols + + # Procesar por lotes para permitir interrupción con barra de progreso + from PySide.QtCore import QCoreApplication + for idx, x in enumerate(pointsx): col = [] cx = x + corridor_offset + # Actualizar barra de progreso cada 20 columnas + if idx % 20 == 0 and hasattr(self.form, 'progressBar'): + self.form.progressBar.setValue(int(100 * idx / n_cols)) + QCoreApplication.processEvents() + if mode == 'aligned' and len(pointsy) > 0: - ref_frame = footprints[0][0] + half_len = ref_len / 2 for y in pointsy: - tp = FreeCAD.Vector(cx, y - ref_frame.Length.Value / 2, 0.0) - found = False + py = y - half_len + # Vector creado solo si pasa el BoundBox check (ya lo hace isInside internamente) + tp = FreeCAD.Vector(cx, py, 0.0) + placed = False if self.isInside(ref_frame, tp): col.append([ref_frame, tp]) - found = True + placed = True else: - for fi, (fr, _) in enumerate(footprints[1:], 1): - ld = (ref_frame.Length.Value - fr.Length.Value) / 2 + # frames alternativos: probar con offsets + for fi in range(1, len(footprints)): + fr = footprints[fi][0] + ld = (ref_len - fr.Length.Value) / 2 for yoff in (ld, -ld): - tp2 = FreeCAD.Vector(tp.x, tp.y + yoff, 0.0) - if self.isInside(fr, tp2): - col.append([fr, tp2]) - found = True + if self.isInside(fr, FreeCAD.Vector(tp.x, tp.y + yoff, 0.0)): + col.append([fr, FreeCAD.Vector(tp.x, tp.y + yoff, 0.0)]) + placed = True break - if found: + if placed: break - if not found: + if not placed: col.append(0) else: + # Non-aligned: intersección de línea vertical con el área line = Part.LineSegment( FreeCAD.Vector(cx, area_ymax, 0.0), FreeCAD.Vector(cx, area_ymin, 0.0) @@ -1591,12 +1625,13 @@ class _PVPlantPlacementTaskPanel: except Exception as e: FreeCAD.Console.PrintWarning(f"Segment error: {e}\n") + # Corredores if corridor_enabled and col: corridor_count += 1 if corridor_count >= self.form.editColCount.value(): corridor_offset += corridor_val corridor_count = 0 - cols.append(col) + cols[idx] = col return self.adjustToTerrain(cols) @@ -1604,33 +1639,27 @@ class _PVPlantPlacementTaskPanel: from datetime import datetime starttime = datetime.now() - # Document optimization params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Document") auto_save_enabled = params.GetBool("AutoSaveEnabled") params.SetBool("AutoSaveEnabled", False) FreeCAD.ActiveDocument.RecomputesFrozen = True try: - # Get selected frames items = [ FreeCAD.ActiveDocument.getObject(self.form.listFrameSetups.item(i).text()) for i in range(self.form.listFrameSetups.count()) if self.form.listFrameSetups.item(i).checkState() == QtCore.Qt.Checked ] - # Remove duplicates efficiently - self.FrameSetups = list({frame.Length.Value: frame for frame in items}.values()) + self.FrameSetups = list({f.Length.Value: f for f in items}.values()) self.FrameSetups.sort(key=lambda x: x.Length.Value, reverse=True) - # Parse parameters self.gap_col = FreeCAD.Units.Quantity(self.form.editGapCols.text()).Value self.gap_row = FreeCAD.Units.Quantity(self.form.editGapRows.text()).Value + self.FrameSetups[0].Length.Value self.offsetX = FreeCAD.Units.Quantity(self.form.editOffsetHorizontal.text()).Value self.offsetY = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value FreeCAD.ActiveDocument.openTransaction("Create Placement") - - # Main processing self.calculateWorkingArea() if self.form.cbAlignFrames.isChecked(): @@ -1641,7 +1670,6 @@ class _PVPlantPlacementTaskPanel: if not dataframe.empty: self.createFrameFromPoints(dataframe) - # Group trackers import Electrical.group as egroup import importlib importlib.reload(egroup) @@ -1650,12 +1678,11 @@ class _PVPlantPlacementTaskPanel: FreeCAD.ActiveDocument.commitTransaction() finally: - # Restore document settings FreeCAD.ActiveDocument.RecomputesFrozen = False params.SetBool("AutoSaveEnabled", auto_save_enabled) - total_time = datetime.now() - starttime - print(f" -- Total time: {total_time}") + elapsed = datetime.now() - starttime + FreeCAD.Console.PrintMessage(f"Placement: {elapsed}\n") FreeCADGui.Control.closeDialog() FreeCAD.ActiveDocument.recompute()