Files
PVPlant/Civil/PVPlantPlatform.py
T

284 lines
8.7 KiB
Python
Raw Normal View History

# /**********************************************************************
# * *
# * Copyright (c) 2026 Javier Braña <javier.branagutierrez@gmail.com> *
# * *
# * Platform - Generación de plataforma desde trackers *
# * *
# * Construye la superficie diseñada (plataforma) a partir de la *
# * disposición de trackers solares, incluyendo pendientes E-W, *
# * conexiones entre filas y columnas. *
# * *
# * Esta plataforma se usa luego en EarthWorks para calcular *
# * volúmenes de corte y relleno contra el terreno natural. *
# * *
# ***********************************************************************
import FreeCAD
import Part
import math
def get_tracker_rows(frames):
"""
Agrupa trackers en filas y columnas usando la lógica de Placement.
Returns:
(rows, columns): listas de listas, o (None, None) si falla
"""
try:
import PVPlantPlacement
return PVPlantPlacement.getRows(frames)
except Exception:
return None, None
def build_platform(frames, slope_tolerance=10.0):
"""
Construye la plataforma (superficie diseñada) a partir de una lista
de frames/trackers.
Args:
frames: lista de objetos tracker (con Placement, Setup, etc.)
slope_tolerance: pendiente máxima E-W en grados
Returns:
Part.Solid con la plataforma, o None si no se puede generar
"""
rows, columns = get_tracker_rows(frames)
if rows is None or not rows:
FreeCAD.Console.PrintWarning(
"No se pudieron agrupar los trackers en filas/columnas\n")
return None
all_faces = []
tools = []
# --- Fase 1: Lofts longitudinales (a lo largo de cada fila) ---
for group in rows:
lines = _generate_row_lines(group, slope_tolerance)
tools.extend(lines["tools"])
if len(lines["edges"]) >= 2:
try:
loft = Part.makeLoft(lines["edges"], False, True, False)
if loft and not loft.isNull():
all_faces.extend(loft.Faces)
except Exception:
pass
# --- Fase 2: Lofts transversales (entre columnas) ---
if columns:
for group in rows:
for frame in group:
col, idx = _find_in_columns(frame, columns)
tool = _find_tool(frame, tools)
if tool is None or idx >= len(col) - 1:
continue
next_frame = col[idx + 1]
next_tool = _find_tool(next_frame, tools)
if next_tool is None:
continue
try:
# Conectar borde izquierdo de frame con borde izquierdo del siguiente
l1 = Part.LineSegment(
tool[1].Vertexes[-1].Point,
next_tool[1].Vertexes[0].Point
).toShape()
# Conectar borde derecho
l2 = Part.LineSegment(
tool[2].Vertexes[-1].Point,
next_tool[2].Vertexes[0].Point
).toShape()
if l1 and l2:
loft = Part.makeLoft([l1, l2], False, True, False)
if loft and not loft.isNull():
all_faces.extend(loft.Faces)
except Exception:
pass
if not all_faces:
FreeCAD.Console.PrintWarning(
"No se generaron caras para la plataforma\n")
return None
# --- Fase 3: Unir caras en un sólido ---
try:
import Utils.PVPlantUtils as utils
# Intentar fuse progresivo
platform = None
for face in all_faces:
if platform is None:
platform = face
else:
try:
platform = platform.fuse(face)
except Exception:
pass
if platform is None:
return None
# Si es una shell, convertir a sólido
if platform.ShapeType == "Shell":
try:
platform = Part.makeSolid(platform)
except Exception:
pass
elif platform.ShapeType == "Compound":
# Extraer caras y hacer shell
faces_in_compound = []
for sub in platform.SubShapes:
if sub.ShapeType == "Face":
faces_in_compound.append(sub)
if faces_in_compound:
try:
shell = Part.makeShell(faces_in_compound)
platform = Part.makeSolid(shell)
except Exception:
pass
return platform if not platform.isNull() else None
except Exception as e:
FreeCAD.Console.PrintError(
f"Error al unir la plataforma: {e}\n")
return None
def add_platform_to_doc(platform, name="Platform"):
"""
Añade la plataforma como objeto visible en el documento activo.
Args:
platform: Part.Shape (Solid o Compound)
name: nombre del objeto en el documento
"""
if platform is None:
return None
doc = FreeCAD.ActiveDocument
if doc is None:
return None
obj = doc.addObject("Part::Feature", name)
obj.Shape = platform
obj.Label = name
doc.recompute()
return obj
def _generate_row_lines(group, slope_tolerance):
"""
Genera las líneas de borde (izquierda/derecha) para una fila de trackers.
Returns:
dict con:
- edges: lista de Part.Shape (líneas)
- tools: lista de [frame, izq, der] para reconexión
"""
lines = {"edges": [], "tools": []}
for i, frame in enumerate(group):
if not hasattr(frame, "Setup"):
continue
aw = _angle_to_prev(group, i)
ae = _angle_to_next(group, i)
anf = (aw + ae) / 2
if anf > slope_tolerance:
anf = slope_tolerance
wdt = _get_tracker_width(frame)
zz = wdt * math.sin(math.radians(anf))
base_line = _get_tracker_base_line(frame)
# Borde izquierdo (sur)
li = base_line.copy()
li.Placement = frame.Placement
li.Placement.Rotation = frame.Placement.Rotation
li.Placement.Base.x -= wdt
li.Placement.Base.z -= zz
lines["edges"].append(li)
# Borde derecho (norte)
ld = base_line.copy()
ld.Placement = frame.Placement
ld.Placement.Rotation = frame.Placement.Rotation
ld.Placement.Base.x += wdt
ld.Placement.Base.z += zz
lines["edges"].append(ld)
lines["tools"].append([frame, li, ld])
return lines
def _get_tracker_width(frame):
"""Ancho medio del tracker (mm)."""
try:
return int(frame.Setup.Width / 2)
except Exception:
return 0
def _get_tracker_base_line(frame):
"""
Línea base longitudinal del tracker, centrada en su origen local.
"""
try:
lng = int(frame.Setup.Length / 2)
return Part.LineSegment(
FreeCAD.Vector(-lng, 0, 0),
FreeCAD.Vector(lng, 0, 0)
).toShape()
except Exception:
# Fallback: BoundBox
try:
bb = frame.Setup.Shape.BoundBox
return Part.LineSegment(
FreeCAD.Vector(bb.XMin, 0, 0),
FreeCAD.Vector(bb.XMax, 0, 0)
).toShape()
except Exception:
return Part.LineSegment(
FreeCAD.Vector(-2000, 0, 0),
FreeCAD.Vector(2000, 0, 0)
).toShape()
def _angle_to_prev(group, i):
if i <= 0:
return 0
p0 = FreeCAD.Vector(group[i - 1].Placement.Base)
p1 = FreeCAD.Vector(group[i].Placement.Base)
return _angle_xz(p0, p1)
def _angle_to_next(group, i):
if i >= len(group) - 1:
return 0
p1 = FreeCAD.Vector(group[i].Placement.Base)
p2 = FreeCAD.Vector(group[i + 1].Placement.Base)
return _angle_xz(p1, p2)
def _angle_xz(v1, v2):
"""Ángulo en el plano XZ entre dos vectores (grados)."""
dx = v2.x - v1.x
dz = v2.z - v1.z
return math.degrees(math.atan2(dz, dx))
def _find_in_columns(frame, columns):
for col in columns:
for group in col:
if frame in group:
return group, group.index(frame)
return [], -1
def _find_tool(frame, tools):
for t in tools:
if t[0] == frame:
return t
return None