144 lines
4.9 KiB
Python
144 lines
4.9 KiB
Python
|
|
# /**********************************************************************
|
||
|
|
# * *
|
||
|
|
# * Copyright (c) 2026 Javier Braña <javier.branagutierrez@gmail.com> *
|
||
|
|
# * *
|
||
|
|
# * Sistema de Alineamiento Horizontal y Vertical para carreteras *
|
||
|
|
# * *
|
||
|
|
# ***********************************************************************
|
||
|
|
|
||
|
|
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.
|
||
|
|
|
||
|
|
Propiedades:
|
||
|
|
- SourceWire: Polilínea base (2D o 3D)
|
||
|
|
- Stations: Lista de estaciones (progresivas)
|
||
|
|
- HorizontalCurves: Curvas circulares en el plano
|
||
|
|
- VerticalProfile: Perfil longitudinal (pendientes + curvas verticales)
|
||
|
|
- CrossSections: Secciones transversales asociadas
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, obj):
|
||
|
|
obj.Proxy = self
|
||
|
|
self.setProperties(obj)
|
||
|
|
self._cached_chainage = None
|
||
|
|
self._cached_points = None
|
||
|
|
|
||
|
|
def setProperties(self, obj):
|
||
|
|
pl = obj.PropertiesList
|
||
|
|
if "SourceWire" not in pl:
|
||
|
|
obj.addProperty("App::PropertyLink",
|
||
|
|
"SourceWire",
|
||
|
|
"Alignment",
|
||
|
|
"Polilínea base del eje").SourceWire = None
|
||
|
|
if "Stations" not in pl:
|
||
|
|
obj.addProperty("App::PropertyFloatList",
|
||
|
|
"Stations",
|
||
|
|
"Alignment",
|
||
|
|
"Progresivas (estaciones) del eje")
|
||
|
|
if "StationInterval" not in pl:
|
||
|
|
obj.addProperty("App::PropertyLength",
|
||
|
|
"StationInterval",
|
||
|
|
"Alignment",
|
||
|
|
"Intervalo entre estaciones").StationInterval = 20000 # 20m
|
||
|
|
if "NumberOfStations" not in pl:
|
||
|
|
obj.addProperty("App::PropertyInteger",
|
||
|
|
"NumberOfStations",
|
||
|
|
"Alignment",
|
||
|
|
"Número de estaciones").NumberOfStations = 0
|
||
|
|
obj.setEditorMode("NumberOfStations", 1) # read-only
|
||
|
|
if "TotalLength" not in pl:
|
||
|
|
obj.addProperty("App::PropertyLength",
|
||
|
|
"TotalLength",
|
||
|
|
"Alignment",
|
||
|
|
"Longitud total del eje").TotalLength = 0
|
||
|
|
obj.setEditorMode("TotalLength", 1)
|
||
|
|
|
||
|
|
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
|
||
|
|
total_len = wire.Length
|
||
|
|
obj.TotalLength = total_len
|
||
|
|
|
||
|
|
interval = obj.StationInterval.Value if hasattr(obj.StationInterval, 'Value') else obj.StationInterval
|
||
|
|
if interval <= 0:
|
||
|
|
interval = 20000
|
||
|
|
|
||
|
|
n_stations = max(2, int(total_len / interval) + 1)
|
||
|
|
obj.NumberOfStations = n_stations
|
||
|
|
|
||
|
|
# Generar puntos de estación a lo largo del wire
|
||
|
|
stations = []
|
||
|
|
for i in range(n_stations):
|
||
|
|
param = i / (n_stations - 1)
|
||
|
|
stations.append(param * total_len)
|
||
|
|
|
||
|
|
obj.Stations = stations
|
||
|
|
self._cached_chainage = stations
|
||
|
|
self._cached_points = None
|
||
|
|
|
||
|
|
def get_station_point(self, obj, distance):
|
||
|
|
"""Devuelve el punto en el eje a una distancia (progresiva) dada."""
|
||
|
|
if not obj.SourceWire:
|
||
|
|
return None
|
||
|
|
try:
|
||
|
|
return obj.SourceWire.Shape.valueAt(
|
||
|
|
obj.SourceWire.Shape.getParameterByLength(distance / obj.TotalLength)
|
||
|
|
)
|
||
|
|
except Exception:
|
||
|
|
return None
|
||
|
|
|
||
|
|
def get_tangent_at(self, obj, distance):
|
||
|
|
"""Devuelve el vector tangente en una progresiva."""
|
||
|
|
if not obj.SourceWire:
|
||
|
|
return None
|
||
|
|
try:
|
||
|
|
return obj.SourceWire.Shape.tangentAt(
|
||
|
|
obj.SourceWire.Shape.getParameterByLength(distance / obj.TotalLength)
|
||
|
|
)
|
||
|
|
except Exception:
|
||
|
|
return None
|
||
|
|
|
||
|
|
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
|