403 lines
12 KiB
Python
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") |