233 lines
7.6 KiB
Python
233 lines
7.6 KiB
Python
|
|
# /**********************************************************************
|
||
|
|
# * *
|
||
|
|
# * Copyright (c) 2026 Javier Braña <javier.branagutierrez@gmail.com> *
|
||
|
|
# * *
|
||
|
|
# * Alignment - Alineamiento horizontal y vertical *
|
||
|
|
# * *
|
||
|
|
# * Define el eje de la carretera mediante: *
|
||
|
|
# * - Alineamiento horizontal: polilínea + curvas circulares *
|
||
|
|
# * - Alineamiento vertical: rasante con pendientes y curvas verticales*
|
||
|
|
# * - Estaciones (progresivas) *
|
||
|
|
# * *
|
||
|
|
# ***********************************************************************
|
||
|
|
|
||
|
|
import FreeCAD
|
||
|
|
import Part
|
||
|
|
import math
|
||
|
|
import numpy as np
|
||
|
|
|
||
|
|
|
||
|
|
def make_alignment_from_wire(wire, name="Alignment"):
|
||
|
|
"""Crea un objeto Alignment a partir de un wire de FreeCAD."""
|
||
|
|
if FreeCAD.ActiveDocument is None:
|
||
|
|
return None
|
||
|
|
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", name)
|
||
|
|
Alignment(obj)
|
||
|
|
_ViewProviderAlignment(obj.ViewObject)
|
||
|
|
obj.SourceWire = wire
|
||
|
|
obj.Label = name
|
||
|
|
FreeCAD.ActiveDocument.recompute()
|
||
|
|
return obj
|
||
|
|
|
||
|
|
|
||
|
|
class Alignment:
|
||
|
|
"""
|
||
|
|
Alineamiento horizontal + vertical de una carretera.
|
||
|
|
|
||
|
|
Propiedades principales:
|
||
|
|
SourceWire : Polilínea base del eje
|
||
|
|
Stations : Progresivas (lista de distancias)
|
||
|
|
StationInterval : Intervalo entre estaciones
|
||
|
|
TotalLength : Longitud total del eje
|
||
|
|
HorizontalCurves : Curvas circulares (radio, longitud, parámetros)
|
||
|
|
VerticalPVI : Puntos de intersección vertical (PVI) para la rasante
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, obj):
|
||
|
|
obj.Proxy = self
|
||
|
|
self.setProperties(obj)
|
||
|
|
self._cached_chainage = None
|
||
|
|
self._cached_station_points = None
|
||
|
|
self._cached_tangents = None
|
||
|
|
|
||
|
|
def setProperties(self, obj):
|
||
|
|
pl = obj.PropertiesList
|
||
|
|
|
||
|
|
if "SourceWire" not in pl:
|
||
|
|
obj.addProperty("App::PropertyLink",
|
||
|
|
"SourceWire", "Alignment",
|
||
|
|
"Polilínea base del eje")
|
||
|
|
|
||
|
|
if "Stations" not in pl:
|
||
|
|
obj.addProperty("App::PropertyFloatList",
|
||
|
|
"Stations", "Alignment",
|
||
|
|
"Estaciones (progresivas) en mm")
|
||
|
|
|
||
|
|
if "StationInterval" not in pl:
|
||
|
|
obj.addProperty("App::PropertyLength",
|
||
|
|
"StationInterval", "Alignment",
|
||
|
|
"Intervalo entre estaciones").StationInterval = 20000
|
||
|
|
|
||
|
|
if "NumberOfStations" not in pl:
|
||
|
|
obj.addProperty("App::PropertyInteger",
|
||
|
|
"NumberOfStations", "Alignment",
|
||
|
|
"Número de estaciones")
|
||
|
|
obj.setEditorMode("NumberOfStations", 1)
|
||
|
|
|
||
|
|
if "TotalLength" not in pl:
|
||
|
|
obj.addProperty("App::PropertyLength",
|
||
|
|
"TotalLength", "Alignment",
|
||
|
|
"Longitud total del eje")
|
||
|
|
obj.setEditorMode("TotalLength", 1)
|
||
|
|
|
||
|
|
if "HorizontalCurveRadii" not in pl:
|
||
|
|
obj.addProperty("App::PropertyFloatList",
|
||
|
|
"HorizontalCurveRadii", "Alignment",
|
||
|
|
"Radios de curva en cada vértice (0 = recta)")
|
||
|
|
|
||
|
|
if "ShowStations" not in pl:
|
||
|
|
obj.addProperty("App::PropertyBool",
|
||
|
|
"ShowStations", "Alignment",
|
||
|
|
"Mostrar marcas de estación en 3D").ShowStations = False
|
||
|
|
|
||
|
|
def onDocumentRestored(self, obj):
|
||
|
|
self.setProperties(obj)
|
||
|
|
|
||
|
|
def execute(self, obj):
|
||
|
|
"""Calcula estaciones y geometría del alignment."""
|
||
|
|
if not obj.SourceWire or not obj.SourceWire.Shape:
|
||
|
|
return
|
||
|
|
|
||
|
|
wire = obj.SourceWire.Shape
|
||
|
|
if wire.isNull():
|
||
|
|
return
|
||
|
|
|
||
|
|
total_len = wire.Length
|
||
|
|
obj.TotalLength = total_len
|
||
|
|
if total_len <= 0:
|
||
|
|
return
|
||
|
|
|
||
|
|
interval = obj.StationInterval.Value
|
||
|
|
if interval <= 0:
|
||
|
|
interval = 20000
|
||
|
|
|
||
|
|
n_stations = max(2, int(total_len / interval) + 1)
|
||
|
|
obj.NumberOfStations = n_stations
|
||
|
|
|
||
|
|
stations = np.linspace(0, total_len, n_stations).tolist()
|
||
|
|
obj.Stations = stations
|
||
|
|
|
||
|
|
# Calcular radios de curva en cada vértice del wire
|
||
|
|
self._compute_curve_radii(obj, wire)
|
||
|
|
|
||
|
|
# Cache
|
||
|
|
self._cached_chainage = stations
|
||
|
|
self._cached_station_points = None
|
||
|
|
self._cached_tangents = None
|
||
|
|
|
||
|
|
def _compute_curve_radii(self, obj, wire):
|
||
|
|
"""Calcula el radio de curvatura en cada vértice de la polilínea."""
|
||
|
|
vertices = wire.Vertexes
|
||
|
|
n = len(vertices)
|
||
|
|
if n < 3:
|
||
|
|
obj.HorizontalCurveRadii = []
|
||
|
|
return
|
||
|
|
|
||
|
|
radii = []
|
||
|
|
for i in range(1, n - 1):
|
||
|
|
p0 = vertices[i - 1].Point
|
||
|
|
p1 = vertices[i].Point
|
||
|
|
p2 = vertices[i + 1].Point
|
||
|
|
|
||
|
|
v1 = p1 - p0
|
||
|
|
v2 = p2 - p1
|
||
|
|
cross = FreeCAD.Vector(0, 0, 1).dot(v1.cross(v2))
|
||
|
|
|
||
|
|
if abs(cross) < 1.0: # casi colineal
|
||
|
|
radii.append(0.0)
|
||
|
|
else:
|
||
|
|
# Radio aproximado = |v1| * |v2| / |v1 x v2|
|
||
|
|
r = v1.Length * v2.Length / abs(cross)
|
||
|
|
radii.append(round(r, 0))
|
||
|
|
|
||
|
|
obj.HorizontalCurveRadii = radii
|
||
|
|
|
||
|
|
def get_station_point(self, obj, distance):
|
||
|
|
"""Devuelve el punto 3D en el eje a una progresiva dada (mm)."""
|
||
|
|
if not obj.SourceWire:
|
||
|
|
return None
|
||
|
|
try:
|
||
|
|
wire = obj.SourceWire.Shape
|
||
|
|
param = wire.getParameterByLength(distance / obj.TotalLength)
|
||
|
|
return wire.valueAt(param)
|
||
|
|
except Exception:
|
||
|
|
return None
|
||
|
|
|
||
|
|
def get_tangent_at(self, obj, distance):
|
||
|
|
"""Devuelve el vector tangente en una progresiva (normalizado)."""
|
||
|
|
if not obj.SourceWire:
|
||
|
|
return None
|
||
|
|
try:
|
||
|
|
wire = obj.SourceWire.Shape
|
||
|
|
param = wire.getParameterByLength(distance / obj.TotalLength)
|
||
|
|
t = wire.tangentAt(param)
|
||
|
|
if t.Length > 0:
|
||
|
|
t.normalize()
|
||
|
|
return t
|
||
|
|
except Exception:
|
||
|
|
return None
|
||
|
|
|
||
|
|
def get_perpendicular_at(self, obj, distance):
|
||
|
|
"""Devuelve el vector perpendicular (horizontal) en una progresiva."""
|
||
|
|
t = self.get_tangent_at(obj, distance)
|
||
|
|
if t is None:
|
||
|
|
return None
|
||
|
|
perp = FreeCAD.Vector(-t.y, t.x, 0)
|
||
|
|
perp.normalize()
|
||
|
|
return perp
|
||
|
|
|
||
|
|
def get_station_data(self, obj):
|
||
|
|
"""
|
||
|
|
Devuelve arrays de (puntos, tangentes, perpendiculares) para todas las estaciones.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
tuple: (points, tangents, perps) listas de FreeCAD.Vector
|
||
|
|
"""
|
||
|
|
stations = obj.Stations
|
||
|
|
if not stations:
|
||
|
|
return [], [], []
|
||
|
|
|
||
|
|
points = []
|
||
|
|
tangents = []
|
||
|
|
perps = []
|
||
|
|
|
||
|
|
for s in stations:
|
||
|
|
pt = self.get_station_point(obj, s)
|
||
|
|
tg = self.get_tangent_at(obj, s)
|
||
|
|
pp = self.get_perpendicular_at(obj, s)
|
||
|
|
if pt and tg and pp:
|
||
|
|
points.append(pt)
|
||
|
|
tangents.append(tg)
|
||
|
|
perps.append(pp)
|
||
|
|
|
||
|
|
return points, tangents, perps
|
||
|
|
|
||
|
|
def __getstate__(self):
|
||
|
|
return None
|
||
|
|
|
||
|
|
def __setstate__(self, state):
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
class _ViewProviderAlignment:
|
||
|
|
def __init__(self, vobj):
|
||
|
|
vobj.Proxy = self
|
||
|
|
|
||
|
|
def getIcon(self):
|
||
|
|
return ""
|
||
|
|
|
||
|
|
def __getstate__(self):
|
||
|
|
return None
|
||
|
|
|
||
|
|
def __setstate__(self, state):
|
||
|
|
return None
|