EarthWorks: separado en PVPlantPlatform (plataforma desde trackers) + PVPlantEarthWorks (solo volumen cut/fill). Platform reutilizable independientemente.
This commit is contained in:
+123
-285
@@ -5,8 +5,14 @@
|
|||||||
# * EarthWorks - Cálculo de movimiento de tierras *
|
# * EarthWorks - Cálculo de movimiento de tierras *
|
||||||
# * *
|
# * *
|
||||||
# * Calcula volúmenes de desmonte (cut) y terraplén (fill) entre una *
|
# * Calcula volúmenes de desmonte (cut) y terraplén (fill) entre una *
|
||||||
# * superficie diseñada y el terreno natural, usando operaciones *
|
# * plataforma diseñada (generada por PVPlantPlatform) y el terreno *
|
||||||
# * booleanas OCC + mallas para visualización. *
|
# * natural representado por un mesh. *
|
||||||
|
# * *
|
||||||
|
# * Flujo: *
|
||||||
|
# * 1. build_platform(frames) → Part.Solid (superficie diseñada) *
|
||||||
|
# * 2. cut_mesh = mesh_above(platform_mesh, terrain_mesh) *
|
||||||
|
# * 3. fill_mesh = mesh_below(platform_mesh, terrain_mesh) *
|
||||||
|
# * 4. Volumen = mesh.Volume *
|
||||||
# * *
|
# * *
|
||||||
# ***********************************************************************
|
# ***********************************************************************
|
||||||
|
|
||||||
@@ -14,8 +20,6 @@ import FreeCAD
|
|||||||
import Part
|
import Part
|
||||||
import Mesh
|
import Mesh
|
||||||
import math
|
import math
|
||||||
import numpy as np
|
|
||||||
import ArchComponent
|
|
||||||
|
|
||||||
if FreeCAD.GuiUp:
|
if FreeCAD.GuiUp:
|
||||||
import FreeCADGui, os
|
import FreeCADGui, os
|
||||||
@@ -27,66 +31,48 @@ else:
|
|||||||
|
|
||||||
import PVPlantResources
|
import PVPlantResources
|
||||||
from PVPlantResources import DirIcons as DirIcons
|
from PVPlantResources import DirIcons as DirIcons
|
||||||
|
from .PVPlantPlatform import build_platform
|
||||||
|
|
||||||
VOLUME_TYPES = ["Fill", "Cut"]
|
VOLUME_TYPES = ["Fill", "Cut"]
|
||||||
|
|
||||||
|
|
||||||
def makeEarthWorksVolume(vtype=0):
|
|
||||||
"""Crea un objeto de volumen (Fill=0, Cut=1) en el documento activo."""
|
|
||||||
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", VOLUME_TYPES[vtype])
|
|
||||||
EarthWorksVolume(obj)
|
|
||||||
ViewProviderEarthWorksVolume(obj.ViewObject)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# Funciones de cálculo principales
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
def get_tracker_rows(frames):
|
|
||||||
"""
|
|
||||||
Agrupa trackers en filas y columnas usando la lógica de Placement.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(rows, columns): listas de listas agrupadas
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
import PVPlantPlacement
|
|
||||||
return PVPlantPlacement.getRows(frames)
|
|
||||||
except Exception:
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
|
|
||||||
def compute_earthworks(frames, terrain_mesh, slope_tolerance=10.0):
|
def compute_earthworks(frames, terrain_mesh, slope_tolerance=10.0):
|
||||||
"""
|
"""
|
||||||
Calcula el movimiento de tierras para una lista de frames/trackers.
|
Calcula el movimiento de tierras para una lista de frames/trackers.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
frames: lista de objetos tracker
|
frames: lista de objetos tracker
|
||||||
terrain_mesh: Mesh del terreno
|
terrain_mesh: Mesh del terreno natural
|
||||||
slope_tolerance: pendiente máxima E-W (grados)
|
slope_tolerance: pendiente máxima E-W (grados)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(mesh_cut, mesh_fill, volume_cut, volume_fill)
|
tuple: (mesh_cut, mesh_fill, volume_cut_mm3, volume_fill_mm3)
|
||||||
|
mesh_cut/mesh_fill pueden ser None si no hay volumen en esa categoría
|
||||||
"""
|
"""
|
||||||
import MeshPart
|
import MeshPart
|
||||||
rows, columns = get_tracker_rows(frames)
|
|
||||||
if rows is None or not rows:
|
# 1. Construir la plataforma (superficie diseñada) desde los trackers
|
||||||
|
platform = build_platform(frames, slope_tolerance)
|
||||||
|
if platform is None:
|
||||||
return None, None, 0, 0
|
return None, None, 0, 0
|
||||||
|
|
||||||
# Generar superficie diseñada (loft de bordes de filas)
|
# 2. Convertir plataforma a mesh para booleanos
|
||||||
design_shape = _build_design_surface(rows, columns, slope_tolerance)
|
try:
|
||||||
if design_shape is None:
|
platform_mesh = MeshPart.meshFromShape(
|
||||||
|
Shape=platform,
|
||||||
|
LinearDeflection=500,
|
||||||
|
AngularDeflection=0.5
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintError(f"Error al meshificar la plataforma: {e}\n")
|
||||||
return None, None, 0, 0
|
return None, None, 0, 0
|
||||||
|
|
||||||
# Convertir a mesh para booleanos
|
if platform_mesh is None or platform_mesh.countPoints() == 0:
|
||||||
design_mesh = MeshPart.meshFromShape(Shape=design_shape, LinearDeflection=500, AngularDeflection=0.5)
|
return None, None, 0, 0
|
||||||
|
|
||||||
# Corte: diseño por encima del terreno → material a remover
|
# 3. Calcular corte y relleno
|
||||||
cut_mesh = _mesh_above(design_mesh, terrain_mesh)
|
cut_mesh = _mesh_above(platform_mesh, terrain_mesh)
|
||||||
|
fill_mesh = _mesh_below(platform_mesh, terrain_mesh)
|
||||||
# Relleno: diseño por debajo del terreno → material a aportar
|
|
||||||
fill_mesh = _mesh_below(design_mesh, terrain_mesh)
|
|
||||||
|
|
||||||
volume_cut = _mesh_volume(cut_mesh)
|
volume_cut = _mesh_volume(cut_mesh)
|
||||||
volume_fill = _mesh_volume(fill_mesh)
|
volume_fill = _mesh_volume(fill_mesh)
|
||||||
@@ -94,181 +80,28 @@ def compute_earthworks(frames, terrain_mesh, slope_tolerance=10.0):
|
|||||||
return cut_mesh, fill_mesh, volume_cut, volume_fill
|
return cut_mesh, fill_mesh, volume_cut, volume_fill
|
||||||
|
|
||||||
|
|
||||||
def _build_design_surface(rows, columns, slope_tolerance):
|
def _mesh_above(reference, terrain):
|
||||||
"""
|
"""
|
||||||
Construye la superficie diseñada entre filas de trackers.
|
Porción del mesh de referencia que está por encima del terreno.
|
||||||
|
Representa material a excavar (cut).
|
||||||
Para cada fila, genera líneas de borde (izquierda/derecha) teniendo
|
|
||||||
en cuenta la pendiente transversal (E-W). Luego lofting para crear
|
|
||||||
la superficie continua entre filas.
|
|
||||||
"""
|
|
||||||
from DraftGeomUtils import isPlanar
|
|
||||||
|
|
||||||
all_lines = []
|
|
||||||
tools = [] # (frame, line_left, line_right)
|
|
||||||
|
|
||||||
for group in rows:
|
|
||||||
lines = []
|
|
||||||
for i, frame in enumerate(group):
|
|
||||||
aw = _angle_to_prev(group, i)
|
|
||||||
ae = _angle_to_next(group, i)
|
|
||||||
anf = (aw + ae) / 2
|
|
||||||
if anf > slope_tolerance:
|
|
||||||
anf = slope_tolerance
|
|
||||||
|
|
||||||
wdt = int(frame.Setup.Width / 2) if hasattr(frame.Setup, 'Width') else 0
|
|
||||||
zz = wdt * math.sin(math.radians(anf))
|
|
||||||
|
|
||||||
# Línea base a lo largo del tracker
|
|
||||||
line = _get_tracker_line(frame)
|
|
||||||
|
|
||||||
# Borde izquierdo (sur)
|
|
||||||
li = line.copy()
|
|
||||||
li.Placement = frame.Placement
|
|
||||||
li.Placement.Rotation = frame.Placement.Rotation
|
|
||||||
li.Placement.Base.x -= wdt
|
|
||||||
li.Placement.Base.z -= zz
|
|
||||||
lines.append(li)
|
|
||||||
|
|
||||||
# Borde derecho (norte)
|
|
||||||
ld = line.copy()
|
|
||||||
ld.Placement = frame.Placement
|
|
||||||
ld.Placement.Rotation = frame.Placement.Rotation
|
|
||||||
ld.Placement.Base.x += wdt
|
|
||||||
ld.Placement.Base.z += zz
|
|
||||||
lines.append(ld)
|
|
||||||
|
|
||||||
tools.append([frame, li, ld])
|
|
||||||
|
|
||||||
if len(lines) >= 2:
|
|
||||||
try:
|
|
||||||
loft = Part.makeLoft(lines, False, True, False)
|
|
||||||
if loft and not loft.isNull():
|
|
||||||
all_lines.append(loft)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Rellenar huecos entre columnas
|
|
||||||
for group in rows:
|
|
||||||
for frame in group:
|
|
||||||
col, idx = _find_in_columns(frame, columns)
|
|
||||||
tool = _find_tool(frame, tools)
|
|
||||||
if tool is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if idx < len(col) - 1:
|
|
||||||
next_frame = col[idx + 1]
|
|
||||||
next_tool = _find_tool(next_frame, tools)
|
|
||||||
if next_tool:
|
|
||||||
try:
|
|
||||||
l1 = Part.LineSegment(tool[1].Vertexes[1].Point,
|
|
||||||
next_tool[1].Vertexes[0].Point).toShape()
|
|
||||||
l2 = Part.LineSegment(tool[2].Vertexes[1].Point,
|
|
||||||
next_tool[2].Vertexes[0].Point).toShape()
|
|
||||||
if len([l1, l2]) >= 2:
|
|
||||||
loft = Part.makeLoft([l1, l2], False, True, False)
|
|
||||||
if loft and not loft.isNull():
|
|
||||||
all_lines.append(loft)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not all_lines:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Unir todas las caras en un solo sólido
|
|
||||||
try:
|
|
||||||
faces = []
|
|
||||||
for item in all_lines:
|
|
||||||
faces.extend(item.Faces)
|
|
||||||
if faces:
|
|
||||||
shell = Part.makeShell(faces)
|
|
||||||
solid = Part.makeSolid(shell)
|
|
||||||
return solid
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _get_tracker_line(frame):
|
|
||||||
"""Obtiene la línea base longitudinal de un tracker."""
|
|
||||||
try:
|
|
||||||
lng = int(frame.Setup.Length / 2)
|
|
||||||
return Part.LineSegment(FreeCAD.Vector(-lng, 0, 0),
|
|
||||||
FreeCAD.Vector(lng, 0, 0)).toShape()
|
|
||||||
except Exception:
|
|
||||||
# Fallback: usar el shape del setup
|
|
||||||
try:
|
|
||||||
setup_shape = frame.Setup.Shape
|
|
||||||
bb = 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):
|
|
||||||
"""Ángulo con el tracker anterior en la fila."""
|
|
||||||
if i <= 0:
|
|
||||||
return 0
|
|
||||||
p0 = FreeCAD.Vector(group[i - 1].Placement.Base)
|
|
||||||
p1 = FreeCAD.Vector(group[i].Placement.Base)
|
|
||||||
return _angle_between(p0, p1)
|
|
||||||
|
|
||||||
|
|
||||||
def _angle_to_next(group, i):
|
|
||||||
"""Ángulo con el tracker siguiente en la fila."""
|
|
||||||
if i >= len(group) - 1:
|
|
||||||
return 0
|
|
||||||
p1 = FreeCAD.Vector(group[i].Placement.Base)
|
|
||||||
p2 = FreeCAD.Vector(group[i + 1].Placement.Base)
|
|
||||||
return _angle_between(p1, p2)
|
|
||||||
|
|
||||||
|
|
||||||
def _angle_between(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):
|
|
||||||
"""Busca un frame en la estructura de columnas."""
|
|
||||||
for col in columns:
|
|
||||||
for group in col:
|
|
||||||
if frame in group:
|
|
||||||
return group, group.index(frame)
|
|
||||||
return [], -1
|
|
||||||
|
|
||||||
|
|
||||||
def _find_tool(frame, tools):
|
|
||||||
"""Busca el tool [frame, line_left, line_right] asociado a un frame."""
|
|
||||||
for t in tools:
|
|
||||||
if t[0] == frame:
|
|
||||||
return t
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _mesh_above(design_mesh, terrain_mesh):
|
|
||||||
"""
|
|
||||||
Devuelve el mesh de diseño que está POR ENCIMA del terreno (Cut).
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
common = design_mesh.common(terrain_mesh)
|
common = reference.common(terrain)
|
||||||
if common and common.countPoints() > 0:
|
if common and common.countPoints() > 3:
|
||||||
return common
|
return common
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _mesh_below(design_mesh, terrain_mesh):
|
def _mesh_below(reference, terrain):
|
||||||
"""
|
"""
|
||||||
Devuelve el mesh de diseño que está POR DEBAJO del terreno (Fill).
|
Porción del mesh de referencia que está por debajo del terreno.
|
||||||
|
Representa material a rellenar (fill).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Mesh booleano: diseño - terreno = parte del diseño fuera del terreno
|
diff = reference.cut(terrain)
|
||||||
diff = design_mesh.cut(terrain_mesh)
|
if diff and diff.countPoints() > 3:
|
||||||
if diff and diff.countPoints() > 0:
|
|
||||||
return diff
|
return diff
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -276,8 +109,8 @@ def _mesh_below(design_mesh, terrain_mesh):
|
|||||||
|
|
||||||
|
|
||||||
def _mesh_volume(mesh):
|
def _mesh_volume(mesh):
|
||||||
"""Calcula el volumen aproximado de un mesh (mm³)."""
|
"""Volumen de un mesh en mm³. Retorna 0 si no hay mesh válido."""
|
||||||
if mesh is None or mesh.countPoints() == 0:
|
if mesh is None or mesh.countPoints() < 4:
|
||||||
return 0
|
return 0
|
||||||
try:
|
try:
|
||||||
return mesh.Volume
|
return mesh.Volume
|
||||||
@@ -285,14 +118,23 @@ def _mesh_volume(mesh):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def makeEarthWorksVolume(vtype=0):
|
||||||
|
"""Crea un objeto FeaturePython con el mesh de volumen."""
|
||||||
|
obj = FreeCAD.ActiveDocument.addObject(
|
||||||
|
"Part::FeaturePython", VOLUME_TYPES[vtype])
|
||||||
|
EarthWorksVolume(obj)
|
||||||
|
ViewProviderEarthWorksVolume(obj.ViewObject)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Objeto EarthWorksVolume (FeaturePython)
|
# FeaturePython: EarthWorksVolume
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
||||||
class EarthWorksVolume(ArchComponent.Component):
|
class EarthWorksVolume:
|
||||||
|
"""Objeto que almacena un mesh de volumen (cut o fill)."""
|
||||||
|
|
||||||
def __init__(self, obj):
|
def __init__(self, obj):
|
||||||
ArchComponent.Component.__init__(self, obj)
|
|
||||||
self.obj = obj
|
|
||||||
self.setProperties(obj)
|
self.setProperties(obj)
|
||||||
obj.Proxy = self
|
obj.Proxy = self
|
||||||
|
|
||||||
@@ -300,32 +142,31 @@ class EarthWorksVolume(ArchComponent.Component):
|
|||||||
pl = obj.PropertiesList
|
pl = obj.PropertiesList
|
||||||
|
|
||||||
if "VolumeType" not in pl:
|
if "VolumeType" not in pl:
|
||||||
obj.addProperty("App::PropertyEnumeration",
|
obj.addProperty(
|
||||||
"VolumeType", "Volume",
|
"App::PropertyEnumeration", "VolumeType", "Volume",
|
||||||
"Fill o Cut").VolumeType = VOLUME_TYPES
|
"Fill o Cut").VolumeType = VOLUME_TYPES
|
||||||
|
|
||||||
if "VolumeMesh" not in pl:
|
if "VolumeMesh" not in pl:
|
||||||
obj.addProperty("Mesh::PropertyMeshKernel",
|
obj.addProperty(
|
||||||
"VolumeMesh", "Volume", "Mesh del volumen")
|
"Mesh::PropertyMeshKernel", "VolumeMesh", "Volume",
|
||||||
|
"Mesh del volumen")
|
||||||
obj.setEditorMode("VolumeMesh", 2)
|
obj.setEditorMode("VolumeMesh", 2)
|
||||||
|
|
||||||
if "Volume" not in pl:
|
if "Volume" not in pl:
|
||||||
obj.addProperty("App::PropertyVolume",
|
obj.addProperty(
|
||||||
"Volume", "Volume",
|
"App::PropertyVolume", "Volume", "Volume",
|
||||||
"Volumen calculado (mm³)")
|
"Volumen calculado (mm³)")
|
||||||
obj.setEditorMode("Volume", 1)
|
obj.setEditorMode("Volume", 1)
|
||||||
|
|
||||||
obj.IfcType = "Civil Element"
|
obj.IfcType = "Civil Element"
|
||||||
obj.setEditorMode("IfcType", 1)
|
obj.setEditorMode("IfcType", 1)
|
||||||
|
|
||||||
def onDocumentRestored(self, obj):
|
def onDocumentRestored(self, obj):
|
||||||
ArchComponent.Component.onDocumentRestored(self, obj)
|
|
||||||
self.setProperties(obj)
|
self.setProperties(obj)
|
||||||
|
|
||||||
def onChange(self, obj, prop):
|
def onChange(self, obj, prop):
|
||||||
if prop == "VolumeMesh":
|
if prop == "VolumeMesh" and obj.VolumeMesh:
|
||||||
if obj.VolumeMesh:
|
obj.Volume = obj.VolumeMesh.Volume
|
||||||
obj.Volume = obj.VolumeMesh.Volume
|
|
||||||
|
|
||||||
def execute(self, obj):
|
def execute(self, obj):
|
||||||
pass
|
pass
|
||||||
@@ -338,7 +179,7 @@ class EarthWorksVolume(ArchComponent.Component):
|
|||||||
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# ViewProvider (Coin3D visualization)
|
# ViewProvider (Coin3D)
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
||||||
class ViewProviderEarthWorksVolume:
|
class ViewProviderEarthWorksVolume:
|
||||||
@@ -348,39 +189,38 @@ class ViewProviderEarthWorksVolume:
|
|||||||
r, g, b = (1.0, 0.0, 0.0) if is_cut else (0.0, 0.0, 1.0)
|
r, g, b = (1.0, 0.0, 0.0) if is_cut else (0.0, 0.0, 1.0)
|
||||||
|
|
||||||
if "Transparency" not in pl:
|
if "Transparency" not in pl:
|
||||||
vobj.addProperty("App::PropertyIntegerConstraint",
|
vobj.addProperty(
|
||||||
"Transparency", "Surface Style",
|
"App::PropertyIntegerConstraint", "Transparency",
|
||||||
"Transparencia de la superficie")
|
"Surface Style", "Transparencia (0=opaco, 100=invisible)")
|
||||||
vobj.Transparency = (50, 0, 100, 1)
|
vobj.Transparency = (50, 0, 100, 1)
|
||||||
|
|
||||||
if "ShapeColor" not in pl:
|
if "ShapeColor" not in pl:
|
||||||
vobj.addProperty("App::PropertyColor",
|
vobj.addProperty(
|
||||||
"ShapeColor", "Surface Style",
|
"App::PropertyColor", "ShapeColor", "Surface Style",
|
||||||
"Color de superficie")
|
"Color de superficie")
|
||||||
vobj.ShapeColor = (r, g, b, vobj.Transparency / 100)
|
vobj.ShapeColor = (r, g, b, vobj.Transparency / 100)
|
||||||
|
|
||||||
if "ShapeMaterial" not in pl:
|
if "ShapeMaterial" not in pl:
|
||||||
vobj.addProperty("App::PropertyMaterial",
|
vobj.addProperty(
|
||||||
"ShapeMaterial", "Surface Style",
|
"App::PropertyMaterial", "ShapeMaterial", "Surface Style",
|
||||||
"Material de superficie")
|
"Material de superficie")
|
||||||
vobj.ShapeMaterial = FreeCAD.Material()
|
vobj.ShapeMaterial = FreeCAD.Material()
|
||||||
|
|
||||||
vobj.Proxy = self
|
vobj.Proxy = self
|
||||||
vobj.ShapeMaterial.DiffuseColor = vobj.ShapeColor
|
vobj.ShapeMaterial.DiffuseColor = vobj.ShapeColor
|
||||||
|
|
||||||
def onChanged(self, vobj, prop):
|
def onChanged(self, vobj, prop):
|
||||||
if prop == "ShapeColor" or prop == "Transparency":
|
if prop in ("ShapeColor", "Transparency"):
|
||||||
if hasattr(vobj, "ShapeColor") and hasattr(vobj, "Transparency"):
|
if hasattr(vobj, "ShapeColor") and hasattr(vobj, "Transparency"):
|
||||||
color = vobj.getPropertyByName("ShapeColor")
|
c = vobj.ShapeColor
|
||||||
t = vobj.getPropertyByName("Transparency")
|
t = vobj.Transparency
|
||||||
vobj.ShapeMaterial.DiffuseColor = (color[0], color[1], color[2], t / 100)
|
vobj.ShapeMaterial.DiffuseColor = (c[0], c[1], c[2], t / 100)
|
||||||
|
|
||||||
if prop == "ShapeMaterial":
|
if prop == "ShapeMaterial":
|
||||||
if hasattr(vobj, "ShapeMaterial"):
|
if hasattr(self, "face_material"):
|
||||||
mat = vobj.getPropertyByName("ShapeMaterial")
|
mat = vobj.ShapeMaterial
|
||||||
if hasattr(self, 'face_material'):
|
self.face_material.diffuseColor.setValue(mat.DiffuseColor[:3])
|
||||||
self.face_material.diffuseColor.setValue(mat.DiffuseColor[:3])
|
self.face_material.transparency = mat.DiffuseColor[3]
|
||||||
self.face_material.transparency = mat.DiffuseColor[3]
|
|
||||||
|
|
||||||
def attach(self, vobj):
|
def attach(self, vobj):
|
||||||
from pivy import coin
|
from pivy import coin
|
||||||
@@ -400,8 +240,8 @@ class ViewProviderEarthWorksVolume:
|
|||||||
offset.styles = coin.SoPolygonOffset.LINES
|
offset.styles = coin.SoPolygonOffset.LINES
|
||||||
offset.factor = -2.0
|
offset.factor = -2.0
|
||||||
|
|
||||||
highlight = coin.SoType.fromName('SoFCSelection').createInstance()
|
highlight = coin.SoType.fromName("SoFCSelection").createInstance()
|
||||||
highlight.style = 'EMISSIVE_DIFFUSE'
|
highlight.style = "EMISSIVE_DIFFUSE"
|
||||||
highlight.addChild(shape_hints)
|
highlight.addChild(shape_hints)
|
||||||
highlight.addChild(mat_binding)
|
highlight.addChild(mat_binding)
|
||||||
highlight.addChild(self.geo_coords)
|
highlight.addChild(self.geo_coords)
|
||||||
@@ -416,15 +256,16 @@ class ViewProviderEarthWorksVolume:
|
|||||||
edge.addChild(self.edge_style)
|
edge.addChild(self.edge_style)
|
||||||
edge.addChild(highlight)
|
edge.addChild(highlight)
|
||||||
|
|
||||||
surface_root = coin.SoSeparator()
|
surface = coin.SoSeparator()
|
||||||
surface_root.addChild(face)
|
surface.addChild(face)
|
||||||
surface_root.addChild(offset)
|
surface.addChild(offset)
|
||||||
surface_root.addChild(edge)
|
surface.addChild(edge)
|
||||||
vobj.addDisplayMode(surface_root, "Surface")
|
|
||||||
|
|
||||||
wireframe_root = coin.SoSeparator()
|
wireframe = coin.SoSeparator()
|
||||||
wireframe_root.addChild(edge)
|
wireframe.addChild(edge)
|
||||||
vobj.addDisplayMode(wireframe_root, "Wireframe")
|
|
||||||
|
vobj.addDisplayMode(surface, "Surface")
|
||||||
|
vobj.addDisplayMode(wireframe, "Wireframe")
|
||||||
|
|
||||||
self.onChanged(vobj, "ShapeColor")
|
self.onChanged(vobj, "ShapeColor")
|
||||||
|
|
||||||
@@ -434,18 +275,17 @@ class ViewProviderEarthWorksVolume:
|
|||||||
if mesh is None or mesh.countPoints() == 0:
|
if mesh is None or mesh.countPoints() == 0:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
geo_system = ["UTM", FreeCAD.ActiveDocument.Site.UtmZone, "FLAT"]
|
geo = ["UTM", FreeCAD.ActiveDocument.Site.UtmZone, "FLAT"]
|
||||||
except Exception:
|
except Exception:
|
||||||
geo_system = ["UTM", "30N", "FLAT"]
|
geo = ["UTM", "30N", "FLAT"]
|
||||||
self.geo_coords.geoSystem.setValues(geo_system)
|
self.geo_coords.geoSystem.setValues(geo)
|
||||||
|
|
||||||
copy_mesh = mesh.copy()
|
cm = mesh.copy()
|
||||||
triangles = []
|
triangles = []
|
||||||
for i in copy_mesh.Topology[1]:
|
for i in cm.Topology[1]:
|
||||||
triangles.extend(list(i))
|
triangles.extend(list(i))
|
||||||
triangles.append(-1)
|
triangles.append(-1)
|
||||||
|
self.geo_coords.point.setValues(cm.Topology[0])
|
||||||
self.geo_coords.point.setValues(copy_mesh.Topology[0])
|
|
||||||
self.triangles.coordIndex.setValues(triangles)
|
self.triangles.coordIndex.setValues(triangles)
|
||||||
|
|
||||||
def getIcon(self):
|
def getIcon(self):
|
||||||
@@ -479,46 +319,47 @@ class EarthWorksTaskPanel:
|
|||||||
QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "convert.svg")))
|
QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "convert.svg")))
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
import MeshPart
|
|
||||||
land = FreeCAD.ActiveDocument.Terrain.Mesh.copy()
|
land = FreeCAD.ActiveDocument.Terrain.Mesh.copy()
|
||||||
|
|
||||||
frames = []
|
frames = []
|
||||||
for obj in FreeCADGui.Selection.getSelection():
|
for obj in FreeCADGui.Selection.getSelection():
|
||||||
if hasattr(obj, "Proxy"):
|
if hasattr(obj, "Proxy"):
|
||||||
proxy_type = getattr(obj.Proxy, "Type", None)
|
t = getattr(obj.Proxy, "Type", None)
|
||||||
if proxy_type == "Tracker":
|
if t == "Tracker" and obj not in frames:
|
||||||
if obj not in frames:
|
frames.append(obj)
|
||||||
frames.append(obj)
|
elif t == "FrameArea":
|
||||||
elif proxy_type == "FrameArea":
|
|
||||||
for fr in obj.Frames:
|
for fr in obj.Frames:
|
||||||
if fr not in frames:
|
if fr not in frames:
|
||||||
frames.append(fr)
|
frames.append(fr)
|
||||||
|
|
||||||
if not frames:
|
if not frames:
|
||||||
FreeCAD.Console.PrintWarning("Selecciona trackers o un FrameArea\n")
|
FreeCAD.Console.PrintWarning(
|
||||||
|
"Selecciona trackers o un FrameArea\n")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
FreeCAD.ActiveDocument.openTransaction("Movimiento de tierras")
|
slope = getattr(FreeCAD.ActiveDocument,
|
||||||
|
"MaximumWestEastSlope", 10.0)
|
||||||
|
|
||||||
|
FreeCAD.ActiveDocument.openTransaction("Movimiento de tierras")
|
||||||
try:
|
try:
|
||||||
cut_mesh, fill_mesh, vol_cut, vol_fill = compute_earthworks(
|
cut_mesh, fill_mesh, vol_cut, vol_fill = compute_earthworks(
|
||||||
frames, land,
|
frames, land, slope)
|
||||||
slope_tolerance=getattr(FreeCAD.ActiveDocument,
|
|
||||||
'MaximumWestEastSlope', 10.0)
|
|
||||||
)
|
|
||||||
|
|
||||||
if cut_mesh and cut_mesh.countPoints() > 0:
|
if cut_mesh and cut_mesh.countPoints() > 3:
|
||||||
vol_obj = makeEarthWorksVolume(1) # Cut
|
v = makeEarthWorksVolume(1) # Cut
|
||||||
vol_obj.VolumeMesh = cut_mesh
|
v.VolumeMesh = cut_mesh
|
||||||
FreeCAD.Console.PrintMessage(f"Volumen corte: {vol_cut:.0f} mm³\n")
|
FreeCAD.Console.PrintMessage(
|
||||||
|
f"Volumen de corte: {vol_cut:,.0f} mm³\n")
|
||||||
|
|
||||||
if fill_mesh and fill_mesh.countPoints() > 0:
|
if fill_mesh and fill_mesh.countPoints() > 3:
|
||||||
vol_obj = makeEarthWorksVolume(0) # Fill
|
v = makeEarthWorksVolume(0) # Fill
|
||||||
vol_obj.VolumeMesh = fill_mesh
|
v.VolumeMesh = fill_mesh
|
||||||
FreeCAD.Console.PrintMessage(f"Volumen relleno: {vol_fill:.0f} mm³\n")
|
FreeCAD.Console.PrintMessage(
|
||||||
|
f"Volumen de relleno: {vol_fill:,.0f} mm³\n")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
FreeCAD.Console.PrintError(f"Error en movimiento de tierras: {e}\n")
|
FreeCAD.Console.PrintError(
|
||||||
|
f"Error en movimiento de tierras: {e}\n")
|
||||||
finally:
|
finally:
|
||||||
FreeCAD.ActiveDocument.commitTransaction()
|
FreeCAD.ActiveDocument.commitTransaction()
|
||||||
|
|
||||||
@@ -528,6 +369,3 @@ class EarthWorksTaskPanel:
|
|||||||
def reject(self):
|
def reject(self):
|
||||||
FreeCADGui.Control.closeDialog()
|
FreeCADGui.Control.closeDialog()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
# Comando registrado desde Init.py o InitGui.py
|
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
# /**********************************************************************
|
||||||
|
# * *
|
||||||
|
# * 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
|
||||||
Reference in New Issue
Block a user