# /********************************************************************** # * * # * Copyright (c) 2026 Javier Braña * # * * # * 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