def groupTrackersToTransformers(transformer_power, max_distance): import numpy as np from scipy.spatial import KDTree import FreeCAD from collections import deque # 1. Obtener todos los trackers válidos valid_trackers = [] valid_points = [] valid_power = [] for tracker in FreeCAD.ActiveDocument.Objects: if hasattr(tracker, 'Proxy') and (tracker.Proxy.Type == "Tracker"): base = tracker.Placement.Base if all(np.isfinite([base.x, base.y, base.z])): valid_trackers.append(tracker) valid_points.append([base.x, base.y]) valid_power.append(tracker.Setup.TotalPower) if not valid_trackers: FreeCAD.Console.PrintWarning("No se encontraron trackers válidos para agrupar\n") return # 2. Obtener parámetros de los CTs target_power = transformer_power * 1.2 points = np.array(valid_points) power_values = np.array(valid_power) # 3. Determinar dirección de barrido (oeste a este por defecto) min_x, min_y = np.min(points, axis=0) max_x, max_y = np.max(points, axis=0) # 4. Ordenar trackers de oeste a este (menor X a mayor X) ''' Norte a Sur: sorted_indices = np.argsort(-points[:, 1]) (Y descendente) Sur a Norte: sorted_indices = np.argsort(points[:, 1]) (Y ascendente) Este a Oeste: sorted_indices = np.argsort(-points[:, 0]) (X descendente) ''' sorted_indices = np.argsort(points[:, 0]) sorted_points = points[sorted_indices] sorted_power = power_values[sorted_indices] sorted_trackers = [valid_trackers[i] for i in sorted_indices] # 5. Crear KDTree para búsquedas rápidas kdtree = KDTree(sorted_points) # 6. Algoritmo de barrido espacial transformer_groups = [] used_indices = set() # Función para expandir un grupo desde un punto inicial def expand_group(start_idx): group = [] total_power = 0 queue = deque([start_idx]) while queue and total_power < target_power: idx = queue.popleft() if idx in used_indices: continue # Añadir tracker al grupo si no excede la potencia tracker_power = sorted_power[idx] if total_power + tracker_power > target_power * 1.05: continue group.append(sorted_trackers[idx]) total_power += tracker_power used_indices.add(idx) # Buscar vecinos cercanos neighbors = kdtree.query_ball_point( sorted_points[idx], max_distance ) # Filtrar vecinos no usados y ordenar por proximidad al punto inicial neighbors = [n for n in neighbors if n not in used_indices] neighbors.sort(key=lambda n: abs(n - start_idx)) queue.extend(neighbors) return group, total_power # 7. Barrido principal de oeste a este for i in range(len(sorted_points)): if i in used_indices: continue group, total_power = expand_group(i) if group: # Calcular centro del grupo group_points = np.array([t.Placement.Base[:2] for t in group]) center = np.mean(group_points, axis=0) transformer_groups.append({ 'trackers': group, 'total_power': total_power, 'center': center }) # 8. Manejar grupos residuales (si los hay) unused_indices = set(range(len(sorted_points))) - used_indices if unused_indices: # Intentar añadir trackers residuales a grupos existentes for idx in unused_indices: point = sorted_points[idx] tracker_power = sorted_power[idx] # Buscar el grupo más cercano que pueda aceptar este tracker best_group = None min_distance = float('inf') for group in transformer_groups: if group['total_power'] + tracker_power <= target_power * 1.05: dist = np.linalg.norm(point - group['center']) if dist < min_distance and dist < max_distance * 1.5: min_distance = dist best_group = group # Añadir al grupo si se encontró uno adecuado if best_group: best_group['trackers'].append(sorted_trackers[idx]) best_group['total_power'] += tracker_power # Actualizar centro del grupo group_points = np.array([t.Placement.Base[:2] for t in best_group['trackers']]) best_group['center'] = np.mean(group_points, axis=0) else: # Crear un nuevo grupo con este tracker residual group = [sorted_trackers[idx]] center = point transformer_groups.append({ 'trackers': group, 'total_power': tracker_power, 'center': center }) # 9. Crear los grupos en FreeCAD transformer_group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "Transformers") transformer_group.Label = "Centros de Transformación" for i, group in enumerate(transformer_groups): # Crear la esfera que representará el CT ct_shape = FreeCAD.ActiveDocument.addObject("Part::Box", f"CT_{i + 1}") ct_shape.Length = 6058 ct_shape.Width = 2438 ct_shape.Height = 2591 ct_shape.Placement.Base = FreeCAD.Vector(group['center'][0], group['center'][1], 0) # Añadir propiedades personalizadas ct_shape.addProperty("App::PropertyLinkList", "Trackers", "CT", "Lista de trackers asociados a este CT") ct_shape.addProperty("App::PropertyFloat", "TotalPower", "CT", "Potencia total del grupo (W)") ct_shape.addProperty("App::PropertyFloat", "NominalPower", "CT", "Potencia nominal del transformador (W)") ct_shape.addProperty("App::PropertyFloat", "Utilization", "CT", "Porcentaje de utilización (Total/Nominal)") # Establecer valores de las propiedades ct_shape.Trackers = group['trackers'] ct_shape.TotalPower = group['total_power'].Value ct_shape.NominalPower = transformer_power ct_shape.Utilization = (group['total_power'].Value / transformer_power) * 100 # Configurar visualización # Calcular color basado en utilización (verde < 100%, amarillo < 110%, rojo > 110%) utilization = ct_shape.Utilization if utilization <= 100: color = (0.0, 1.0, 0.0) # Verde elif utilization <= 110: color = (1.0, 1.0, 0.0) # Amarillo else: color = (1.0, 0.0, 0.0) # Rojo ct_shape.ViewObject.ShapeColor = color ct_shape.ViewObject.Transparency = 40 # 40% de transparencia # Añadir etiqueta con información ct_shape.ViewObject.DisplayMode = "Shaded" ct_shape.Label = f"CT {i + 1} ({ct_shape.TotalPower / 1000:.1f}kW/{ct_shape.NominalPower / 1000:.1f}kW)" # Añadir al grupo principal transformer_group.addObject(ct_shape) FreeCAD.Console.PrintMessage(f"Se crearon {len(transformer_groups)} centros de transformación\n") #onSelectGatePoint() import FreeCAD, FreeCADGui, Part import numpy as np from scipy.stats import linregress from PySide import QtGui class InternalPathCreator: def __init__(self, gate_point, strategy=1, path_width=4000): self.gate_point = gate_point self.strategy = strategy self.path_width = path_width self.ct_shapes = [] self.ct_positions = [] def get_transformers(self): transformers_group = FreeCAD.ActiveDocument.getObject("Transformers") if not transformers_group: FreeCAD.Console.PrintError("No se encontró el grupo 'Transformers'\n") return False self.ct_shapes = transformers_group.Group if not self.ct_shapes: FreeCAD.Console.PrintWarning("No hay Centros de Transformación en el grupo\n") return False # Obtener las posiciones de los CTs for sphere in self.ct_shapes: base = sphere.Placement.Base self.ct_positions.append(FreeCAD.Vector(base.x, base.y, 0)) return True def create_paths(self): if not self.get_transformers(): return [] if self.strategy == 1: return self.create_direct_paths() elif self.strategy == 2: return self.create_unified_path() else: FreeCAD.Console.PrintError("Estrategia no válida. Use 1 o 2.\n") return [] def create_direct_paths(self): """Estrategia 1: Caminos independientes desde cada CT hasta la puerta""" paths = [] for ct in self.ct_positions: paths.append([ct, self.gate_point]) return paths def create_unified_path(self): """Estrategia 2: Único camino que une todos los CTs y la puerta usando regresión lineal""" if not self.ct_positions: return [] all_points = self.ct_positions + [self.gate_point] x = [p.x for p in all_points] y = [p.y for p in all_points] # Manejar caso de puntos alineados verticalmente if np.std(x) < 1e-6: sorted_points = sorted(all_points, key=lambda p: p.y) paths = [] for i in range(len(sorted_points) - 1): paths.append([sorted_points[i], sorted_points[i + 1]]) return paths # Calcular regresión lineal slope, intercept, _, _, _ = linregress(x, y) # Función para proyectar puntos def project_point(point): x0, y0 = point.x, point.y if abs(slope) > 1e6: return FreeCAD.Vector(x0, intercept, 0) x_proj = (x0 + slope * (y0 - intercept)) / (1 + slope ** 2) y_proj = slope * x_proj + intercept return FreeCAD.Vector(x_proj, y_proj, 0) # return slope * x + intercept --> desde placement projected_points = [project_point(p) for p in all_points] # Calcular distancias a lo largo de la línea ref_point = projected_points[0] direction_vector = FreeCAD.Vector(1, slope).normalize() distances = [] for p in projected_points: vec_to_point = p - ref_point distance = vec_to_point.dot(direction_vector) distances.append(distance) # Ordenar por distancia sorted_indices = np.argsort(distances) sorted_points = [all_points[i] for i in sorted_indices] # Crear caminos paths = [] for i in range(len(sorted_points) - 1): paths.append([sorted_points[i], sorted_points[i + 1]]) return paths def create_3d_path(self, path_poly): """Crea geometría 3D para el camino adaptada a orientación norte-sur""" segments = [] for i in range(len(path_poly.Vertexes) - 1): start = path_poly.Vertexes[i].Point end = path_poly.Vertexes[i + 1].Point direction = end - start # Determinar orientación predominante if abs(direction.x) > abs(direction.y): normal = FreeCAD.Vector(0, 1, 0) # Norte-sur else: normal = FreeCAD.Vector(1, 0, 0) # Este-oeste offset = normal * self.path_width / 2 # Crear puntos para la sección transversal p1 = start + offset p2 = start - offset p3 = end - offset p4 = end + offset # Crear cara wire = Part.makePolygon([p1, p2, p3, p4, p1]) face = Part.Face(wire) segments.append(face) if segments: '''road_shape = segments[0].fuse(segments[1:]) return road_shape.removeSplitter()''' return Part.makeCompound(segments) return Part.Shape() def build(self): paths = self.create_paths() if not paths: return path_group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "InternalPaths") path_group.Label = f"Caminos Internos (Estrategia {self.strategy})" for i, path in enumerate(paths): poly = Part.makePolygon(path) # Objeto para la línea central path_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"Path_{i + 1}") path_obj.Shape = poly path_obj.ViewObject.LineWidth = 3.0 path_obj.ViewObject.LineColor = (0.0, 0.0, 1.0) # Objeto para la superficie 3D road_shape = self.create_3d_path(poly) road_obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"Road_{i + 1}") road_obj.Shape = road_shape road_obj.ViewObject.ShapeColor = (0.7, 0.7, 0.7) path_group.addObject(path_obj) path_group.addObject(road_obj) FreeCAD.Console.PrintMessage(f"Se crearon {len(paths)} segmentos de caminos internos\n") # Función para mostrar el diálogo de estrategia def show_path_strategy_dialog(gate_point): dialog = QtGui.QDialog() dialog.setWindowTitle("Seleccionar Estrategia de Caminos") layout = QtGui.QVBoxLayout(dialog) label = QtGui.QLabel("Seleccione la estrategia para crear los caminos internos:") layout.addWidget(label) rb1 = QtGui.QRadioButton("Estrategia 1: Caminos independientes desde cada CT hasta la puerta") rb1.setChecked(True) layout.addWidget(rb1) rb2 = QtGui.QRadioButton("Estrategia 2: Único camino que une todos los CTs y la puerta") layout.addWidget(rb2) btn_box = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel) layout.addWidget(btn_box) def on_accept(): strategy = 1 if rb1.isChecked() else 2 dialog.accept() creator = InternalPathCreator(gate_point, strategy) creator.build() btn_box.accepted.connect(on_accept) btn_box.rejected.connect(dialog.reject) dialog.exec_() # Uso: seleccionar un punto para la puerta de entrada def onSelectGatePoint(): '''gate = FreeCAD.ActiveDocument.findObjects(Name="FenceGate")[0] gate_point = gate.Placement.Base show_path_strategy_dialog(gate_point)''' sel = FreeCADGui.Selection.getSelectionEx()[0] show_path_strategy_dialog(sel.SubObjects[0].CenterOfMass)