Files
PVPlant/Civil/PVPlantPlacementCalc.py
T

403 lines
12 KiB
Python

# /**********************************************************************
# * *
# * 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")