Placement: limpieza masiva. Eliminadas clases _old y _new1 (~550 líneas). Eliminadas funciones globales (~600 líneas). Movidas a Civil/PVPlantPlacementCalc.py. PVPlantPlacement.py pasa de 2352→663 líneas solo con TaskPanels y comandos.
This commit is contained in:
@@ -0,0 +1,403 @@
|
||||
# /**********************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2026 Javier Braña <javier.branagutierrez@gmail.com> *
|
||||
# * *
|
||||
# * 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")
|
||||
+10
-1699
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user