# /********************************************************************** # * * # * Copyright (c) 2021-2026 Javier Braña * # * * # * PVPlant Road - Sistema de carreteras con alineamiento profesional * # * Basado en ejes (Alignment) con estaciones, perfiles y cubicación. * # * * # *********************************************************************** import FreeCAD import ArchComponent import Part import math import numpy as np if FreeCAD.GuiUp: import FreeCADGui from PySide import QtCore from DraftTools import translate from PySide.QtCore import QT_TRANSLATE_NOOP import os else: def translate(ctxt, txt): return txt def QT_TRANSLATE_NOOP(ctxt, txt): return txt import PVPlantResources from PVPlantResources import DirIcons as DirIcons from Civil.Alignment import make_alignment_from_wire def makeRoad(base=None, alignment=None): """Crea un objeto Road con o sin alignment.""" doc = FreeCAD.ActiveDocument obj = doc.addObject("Part::FeaturePython", "Road") _Road(obj) _ViewProviderRoad(obj.ViewObject) obj.Base = base obj.Alignment = alignment doc.recompute() return obj class _Road(ArchComponent.Component): """Carretera con alineamiento horizontal+vertical y secciones multicapa.""" def __init__(self, obj): ArchComponent.Component.__init__(self, obj) self.obj = obj self.setProperties(obj) self.Type = "Road" obj.Proxy = self obj.IfcType = "Civil Element" obj.setEditorMode("IfcType", 1) def setProperties(self, obj): pl = obj.PropertiesList # --- Alineamiento --- if "Alignment" not in pl: obj.addProperty("App::PropertyLink", "Alignment", "Road", "Objeto Alignment que define el eje").Alignment = None if "Base" not in pl: obj.addProperty("App::PropertyLink", "Base", "Road", "Wire base (alternativo si no hay Alignment)").Base = None # --- Geometría transversal --- if "Width" not in pl: obj.addProperty("App::PropertyLength", "Width", "Road", "Ancho total de la carretera").Width = 4000 if "PavementThickness" not in pl: obj.addProperty("App::PropertyLength", "PavementThickness", "Road", "Espesor del pavimento").PavementThickness = 250 if "BaseThickness" not in pl: obj.addProperty("App::PropertyLength", "BaseThickness", "Road", "Espesor de la base").BaseThickness = 200 if "SubbaseThickness" not in pl: obj.addProperty("App::PropertyLength", "SubbaseThickness", "Road", "Espesor de la subbase").SubbaseThickness = 300 if "ShoulderWidth" not in pl: obj.addProperty("App::PropertyLength", "ShoulderWidth", "Road", "Ancho del arcén cada lado").ShoulderWidth = 500 if "CrossSlope" not in pl: obj.addProperty("App::PropertyPercent", "CrossSlope", "Road", "Pendiente transversal del pavimento (%)").CrossSlope = 2 if "DitchSlope" not in pl: obj.addProperty("App::PropertyPercent", "DitchSlope", "Road", "Pendiente del drenaje (%)").DitchSlope = 3 # --- Estaciones y cubicación --- if "StationInterval" not in pl: obj.addProperty("App::PropertyLength", "StationInterval", "Road", "Intervalo entre estaciones de cálculo").StationInterval = 20000 if "NumberOfStations" not in pl: obj.addProperty("App::PropertyInteger", "NumberOfStations", "Road", "Número de estaciones calculadas").NumberOfStations = 0 obj.setEditorMode("NumberOfStations", 1) if "CutVolume" not in pl: obj.addProperty("App::PropertyVolume", "CutVolume", "Road", "Volumen de desmonte (corte)").CutVolume = 0 obj.setEditorMode("CutVolume", 1) if "FillVolume" not in pl: obj.addProperty("App::PropertyVolume", "FillVolume", "Road", "Volumen de terraplén (relleno)").FillVolume = 0 obj.setEditorMode("FillVolume", 1) if "TotalLength" not in pl: obj.addProperty("App::PropertyLength", "TotalLength", "Road", "Longitud total del eje").TotalLength = 0 obj.setEditorMode("TotalLength", 1) def onDocumentRestored(self, obj): ArchComponent.Component.onDocumentRestored(self, obj) self.obj = obj self.Type = "Road" obj.Proxy = self def _get_alignment_wire(self, obj): """Devuelve el wire base (desde Alignment o Base).""" if obj.Alignment and obj.Alignment.SourceWire: return obj.Alignment.SourceWire.Shape if obj.Base: return obj.Base.Shape return None def _generate_cross_section(self, obj, station_point, tangent): """ Genera el perfil transversal en un punto del eje. Returns: list of Part.Wire: [pavimento, base, subbase, arcén_izq, arcén_der] """ # Ancho medio carril hw = obj.Width.Value / 2 sw = obj.ShoulderWidth.Value cs = obj.CrossSlope / 100 # pendiente transversal (decimal) ds = obj.DitchSlope / 100 pt = obj.PavementThickness.Value bt = obj.BaseThickness.Value sbt = obj.SubbaseThickness.Value # Vector perpendicular (horizontal) al eje perp = FreeCAD.Vector(-tangent.y, tangent.x, 0) perp.normalize() # Puntos del pavimento (sección transversal con bombeo) # Centro del eje center = station_point # Borde izquierdo pavimento left_edge = center + perp * (-hw) right_edge = center + perp * hw # Con pendiente transversal: el centro más alto left_top = FreeCAD.Vector(left_edge.x, left_edge.y, center.z - hw * cs) right_top = FreeCAD.Vector(right_edge.x, right_edge.y, center.z - hw * cs) center_top = center # Borde inferior pavimento left_bot = FreeCAD.Vector(left_top.x, left_top.y, left_top.z - pt) right_bot = FreeCAD.Vector(right_top.x, right_top.y, right_top.z - pt) center_bot = FreeCAD.Vector(center_top.x, center_top.y, center_top.z - pt) # Arcén (más ancho, misma pendiente o ligeramente mayor) shoulder_left = FreeCAD.Vector(left_edge.x - sw, left_edge.y - sw * 0, left_top.z - sw * cs * 0.5) shoulder_right = FreeCAD.Vector(right_edge.x + sw, right_edge.y + sw * 0, right_top.z - sw * cs * 0.5) # Base (ligeiramente más ancha) base_extra = 200 # mm extra cada lado bl = FreeCAD.Vector(left_bot.x - base_extra, left_bot.y, left_bot.z) br = FreeCAD.Vector(right_bot.x + base_extra, right_bot.y, right_bot.z) bc = FreeCAD.Vector(center_bot.x, center_bot.y, center_bot.z) bl_bot = FreeCAD.Vector(bl.x, bl.y, bl.z - bt) br_bot = FreeCAD.Vector(br.x, br.y, br.z - bt) # Subbase (aún más ancha) sbl = FreeCAD.Vector(bl.x - base_extra, bl.y, bl.z) sbr = FreeCAD.Vector(br.x + base_extra, br.y, br.z) sbl_bot = FreeCAD.Vector(sbl.x, sbl.y, sbl.z - sbt) sbr_bot = FreeCAD.Vector(sbr.x, sbr.y, sbr.z - sbt) # Construir wires de cada capa # Pavimento pave = Part.makePolygon([left_top, center_top, right_top, right_bot, center_bot, left_bot, left_top]) # Base base = Part.makePolygon([bl, bc, br, br_bot, bc - FreeCAD.Vector(0, 0, bt), bl_bot, bl]) # Subbase subbase = Part.makePolygon([sbl, sbl + FreeCAD.Vector(0, 0, -sbt), sbr + FreeCAD.Vector(0, 0, -sbt), sbr, sbl]) return [pave, base, subbase] def execute(self, obj): """Genera el sólido 3D de la carretera por extrusión de secciones.""" wire = self._get_alignment_wire(obj) if not wire: return total_len = wire.Length obj.TotalLength = total_len interval = obj.StationInterval.Value if interval <= 0: interval = 20000 n_stations = max(2, int(total_len / interval) + 1) obj.NumberOfStations = n_stations # Generar el sólido mediante barrido de secciones shapes = [] cut_volume = 0 fill_volume = 0 for i in range(n_stations): param = i / (n_stations - 1) try: pt = wire.valueAt(wire.getParameterByLength(param * total_len)) tangent = wire.tangentAt(wire.getParameterByLength(param * total_len)) except Exception: continue sections = self._generate_cross_section(obj, pt, tangent) # Barrer cada sección a lo largo del eje (versión simplificada) for sec in sections: try: # Extrusión simple a lo largo del eje # En una implementación completa: makePipeShell shape = sec.extrude(FreeCAD.Vector(0, 0, 1)) if shape and not shape.isNull(): shapes.append(shape) except Exception: pass if shapes: try: compound = Part.makeCompound(shapes) obj.Shape = compound # Cubicación contra el terreno terrain = self._get_terrain(obj) if terrain: try: common = compound.common(terrain.Shape) if common and not common.isNull(): cut_volume = common.Volume # Terraplén: volumen del sólido fuera del terreno fill = compound.cut(terrain.Shape) if fill and not fill.isNull(): fill_volume = fill.Volume except Exception: pass obj.CutVolume = cut_volume obj.FillVolume = fill_volume except Exception: pass def _get_terrain(self, obj): """Obtiene el terreno desde el Site.""" try: return FreeCAD.ActiveDocument.Site.Terrain except Exception: return None def __getstate__(self): return self.Type def __setstate__(self, state): if state: self.Type = state class _ViewProviderRoad(ArchComponent.ViewProviderComponent): def __init__(self, vobj): ArchComponent.ViewProviderComponent.__init__(self, vobj) def getIcon(self): return str(os.path.join(PVPlantResources.DirIcons, "road.svg")) # --------------------------------------------------------------------------- # TaskPanel para crear carretera interactivamente # --------------------------------------------------------------------------- class _RoadTaskPanel: def __init__(self, obj=None): if obj is None: self.new = True self.obj = makeRoad() else: self.new = False self.obj = obj self.form = FreeCADGui.PySideUic.loadUi( os.path.join(PVPlantResources.__dir__, "PVPlantRoad.ui")) def accept(self): FreeCADGui.Control.closeDialog() return True def reject(self): FreeCAD.ActiveDocument.removeObject(self.obj.Name) if self.new: FreeCADGui.Control.closeDialog() return True # --------------------------------------------------------------------------- # Comando para dibujar carretera sobre un wire seleccionado # --------------------------------------------------------------------------- class _CommandRoad: """Comando para crear carretera seleccionando un wire + generando alignment.""" def GetResources(self): return {'Pixmap': str(os.path.join(DirIcons, "road.svg")), 'MenuText': QT_TRANSLATE_NOOP("PVPlantRoad", "Road"), 'Accel': "C, R", 'ToolTip': QT_TRANSLATE_NOOP("PVPlantRoad", "Crea una carretera con alineamiento profesional.")} def IsActive(self): return FreeCAD.ActiveDocument is not None def Activated(self): sel = FreeCADGui.Selection.getSelection() wire = None if sel: import Draft if Draft.getType(sel[0]) == "Wire": wire = sel[0] if wire: # Crear alignment desde el wire seleccionado alignment = make_alignment_from_wire(wire) road = makeRoad(alignment=alignment) FreeCAD.Console.PrintMessage( f"Carretera creada desde '{wire.Label}'. " f"Alineamiento: {alignment.Label}\n") else: FreeCAD.Console.PrintWarning( "Selecciona un Wire (polilínea) para usarlo como eje de carretera.\n") if FreeCAD.GuiUp: FreeCADGui.addCommand('PVPlantRoad', _CommandRoad())