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")
|
||||||
+9
-1698
File diff suppressed because it is too large
Load Diff
@@ -61,7 +61,7 @@ class SelObserver:
|
|||||||
|
|
||||||
def onAceptClick(self):
|
def onAceptClick(self):
|
||||||
''' '''
|
''' '''
|
||||||
from PVPlantPlacement import moveFrameHead
|
from Civil.PVPlantPlacementCalc import moveFrameHead
|
||||||
moveFrameHead(self.obj, head=self.ui.comboHead.currentIndex(),
|
moveFrameHead(self.obj, head=self.ui.comboHead.currentIndex(),
|
||||||
dist=self.ui.editDist.value())
|
dist=self.ui.editDist.value())
|
||||||
self.setUI(self.obj)
|
self.setUI(self.obj)
|
||||||
|
|||||||
Reference in New Issue
Block a user