From f3f94d4f597e5a6087dddd9f3ec95cfb9424c1d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Bra=C3=B1a?= Date: Sun, 3 May 2026 21:43:18 +0200 Subject: [PATCH] =?UTF-8?q?Road:=20sistema=20de=20alineamiento=20profesion?= =?UTF-8?q?al.=20Alignment=20(eje+estaciones),=20Road=20multicapa=20con=20?= =?UTF-8?q?cubicaci=C3=B3n=20contra=20terreno?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Civil/Alignment.py | 144 +++++++++ PVPlantRoad.py | 786 ++++++++++++++++----------------------------- 2 files changed, 413 insertions(+), 517 deletions(-) create mode 100644 Civil/Alignment.py diff --git a/Civil/Alignment.py b/Civil/Alignment.py new file mode 100644 index 0000000..aa73979 --- /dev/null +++ b/Civil/Alignment.py @@ -0,0 +1,144 @@ +# /********************************************************************** +# * * +# * Copyright (c) 2026 Javier Braña * +# * * +# * 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 \ No newline at end of file diff --git a/PVPlantRoad.py b/PVPlantRoad.py index 6146a9f..2d54fef 100644 --- a/PVPlantRoad.py +++ b/PVPlantRoad.py @@ -1,314 +1,297 @@ +# /********************************************************************** +# * * +# * 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 Part import os else: - # \cond - def translate(ctxt, txt): - return txt - - def QT_TRANSLATE_NOOP(ctxt, txt): - return txt - # \endcond - -__title__ = "PVPlant Road" -__author__ = "Javier Braña" -__url__ = "http://www.sogos-solar.com" + 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): - obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Road") +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 - - from Project.Area import PVPlantArea - offset = PVPlantArea.makeOffsetArea(obj, 4000) - PVPlantArea.makeProhibitedArea(offset) + obj.Alignment = alignment + doc.recompute() return obj class _Road(ArchComponent.Component): + """Carretera con alineamiento horizontal+vertical y secciones multicapa.""" + def __init__(self, obj): - # Definición de Variables: ArchComponent.Component.__init__(self, obj) self.obj = obj self.setProperties(obj) self.Type = "Road" obj.Proxy = self - - self.route = False - - obj.IfcType = "Civil Element" ## puede ser: Cable Carrier Segment + obj.IfcType = "Civil Element" obj.setEditorMode("IfcType", 1) - - self.count = 0 - def setProperties(self, obj): - # Definicion de Propiedades: - '''[ - 'App::PropertyBool', - 'App::PropertyBoolList', - 'App::PropertyFloat', - 'App::PropertyFloatList', - 'App::PropertyFloatConstraint', - 'App::PropertyPrecision', - 'App::PropertyQuantity', - 'App::PropertyQuantityConstraint', - 'App::PropertyAngle', - 'App::PropertyDistance', - 'App::PropertyLength', - 'App::PropertyArea', - 'App::PropertyVolume', - 'App::PropertyFrequency', - 'App::PropertySpeed', - 'App::PropertyAcceleration', - 'App::PropertyForce', - 'App::PropertyPressure', - 'App::PropertyVacuumPermittivity', - 'App::PropertyInteger', - 'App::PropertyIntegerConstraint', - 'App::PropertyPercent', - 'App::PropertyEnumeration', - 'App::PropertyIntegerList', - 'App::PropertyIntegerSet', - 'App::PropertyMap', - 'App::PropertyString', - 'App::PropertyPersistentObject', - 'App::PropertyUUID', - 'App::PropertyFont', - 'App::PropertyStringList', - 'p::PropertyLink', - 'App::PropertyLinkChild', - 'App::PropertyLinkGlobal', - 'App::PropertyLinkHidden', - 'App::PropertyLinkSub', - 'App::PropertyLinkSubChild', - 'App::PropertyLinkSubGlobal', - 'App::PropertyLinkSubHidden', - 'App::PropertyLinkList', - 'App::PropertyLinkListChild', - 'App::PropertyLinkListGlobal', - 'App::PropertyLinkListHidden', - 'App::PropertyLinkSubList', - 'App::PropertyLinkSubListChild', - 'App::PropertyLinkSubListGlobal', - 'App::PropertyLinkSubListHidden', - 'App::PropertyXLink', - 'App::PropertyXLinkSub', - 'App::PropertyXLinkSubList', - 'App::PropertyXLinkList', - 'App::PropertyMatrix', - 'App::PropertyVector', - 'App::PropertyVectorDistance', - 'App::PropertyPosition', - 'App::PropertyDirection', - 'App::PropertyVectorList', - 'App::PropertyPlacement', - 'App::PropertyPlacementList', - 'App::PropertyPlacementLink', - 'App::PropertyColor', - 'App::PropertyColorList', - 'App::PropertyMaterial', - 'App::PropertyMaterialList', - 'App::PropertyPath', - 'App::PropertyFile', - 'App::PropertyFileIncluded', - 'App::PropertyPythonObject', - 'App::PropertyExpressionEngine', - 'Part::PropertyPartShape', - 'Part::PropertyGeometryList', - 'Part::PropertyShapeHistory', - 'Part::PropertyFilletEdges', - 'Mesh::PropertyNormalList', - 'Mesh::PropertyCurvatureList', - 'Mesh::PropertyMeshKernel', - 'Sketcher::PropertyConstraintList' - ]''' + pl = obj.PropertiesList - obj.addProperty("App::PropertyPercent", - "SurfaceSlope", - "Road", - QT_TRANSLATE_NOOP("App::Property", "Connection")).SurfaceSlope = 2 + # --- Alineamiento --- + if "Alignment" not in pl: + obj.addProperty("App::PropertyLink", + "Alignment", "Road", + "Objeto Alignment que define el eje").Alignment = None - obj.addProperty("App::PropertyPercent", - "SurfaceDrainSlope", - "Road", - QT_TRANSLATE_NOOP("App::Property", "Connection")).SurfaceDrainSlope = int(3 / 2 * 100) + if "Base" not in pl: + obj.addProperty("App::PropertyLink", + "Base", "Road", + "Wire base (alternativo si no hay Alignment)").Base = None - obj.addProperty("App::PropertyPercent", - "SubbaseDrainSlope", - "Road", - QT_TRANSLATE_NOOP("App::Property", "Connection")).SubbaseDrainSlope = int(2 / 3 * 100) + # --- Geometría transversal --- + if "Width" not in pl: + obj.addProperty("App::PropertyLength", + "Width", "Road", + "Ancho total de la carretera").Width = 4000 - obj.addProperty("App::PropertyLength", - "Width", - "Road", - QT_TRANSLATE_NOOP("App::Property", "Connection")).Width = 4000 + if "PavementThickness" not in pl: + obj.addProperty("App::PropertyLength", + "PavementThickness", "Road", + "Espesor del pavimento").PavementThickness = 250 - obj.addProperty("App::PropertyLength", - "Height", - "Road", - QT_TRANSLATE_NOOP("App::Property", "Connection")).Height = 250 + if "BaseThickness" not in pl: + obj.addProperty("App::PropertyLength", + "BaseThickness", "Road", + "Espesor de la base").BaseThickness = 200 - obj.addProperty("App::PropertyLength", - "Subbase", - "Road", - QT_TRANSLATE_NOOP("App::Property", "Connection")).Subbase = 400 + 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): - """Method run when the document is restored. - Re-adds the Arch component, and object properties.""" - 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): - import Part, math + """Genera el sólido 3D de la carretera por extrusión de secciones.""" + wire = self._get_alignment_wire(obj) + if not wire: + return - w = obj.Base.Shape - profiles = [] + total_len = wire.Length + obj.TotalLength = total_len + interval = obj.StationInterval.Value + if interval <= 0: + interval = 20000 - SurfaceDrainSlope = obj.SurfaceDrainSlope / 100 - SubbaseDrainSlope = obj.SubbaseDrainSlope / 100 - - vec_up_left = FreeCAD.Vector(-obj.Width.Value / 2, 0, obj.Height.Value) - vec_up_center = FreeCAD.Vector(0, 0, obj.SurfaceSlope * obj.Width.Value / 200 + obj.Height.Value) - vec_up_right = FreeCAD.Vector(obj.Width.Value / 2, 0, obj.Height.Value) - - vec_down_left = FreeCAD.Vector(-(obj.Width.Value / 2 + obj.Height.Value / SurfaceDrainSlope), 0, 0) - vec_down_right = FreeCAD.Vector((obj.Width.Value / 2 + obj.Height.Value / SurfaceDrainSlope), 0, 0) - - vec_sand_left = FreeCAD.Vector(-(obj.Width.Value / 2 + obj.Height.Value * (1 / SurfaceDrainSlope + SubbaseDrainSlope)), 0, - obj.Subbase.Value) - vec_sand_right = FreeCAD.Vector((obj.Width.Value / 2 + obj.Height.Value * (1 / SurfaceDrainSlope + SubbaseDrainSlope)), 0, - obj.Subbase.Value) - - edge1 = Part.makeLine(vec_down_left, vec_down_right) - edge2 = Part.makeLine(vec_down_right, vec_up_right) - edge3 = Part.makeLine(vec_up_right, vec_up_center) - edge4 = Part.makeLine(vec_up_center, vec_up_left) - edge5 = Part.makeLine(vec_up_left, vec_down_left) - - edge6 = Part.makeLine(vec_sand_left, vec_sand_right) - edge7 = Part.makeLine(vec_sand_left, vec_down_left) - edge8 = Part.makeLine(vec_sand_right, vec_down_right) - - p = Part.Wire([edge1, edge2, edge3, edge4, edge5]) - profiles.append(p) - p = Part.Wire([edge6, edge8, edge1, edge7]) - profiles.append(p) - shapes = self.makeSolids(obj, profiles, w, (vec_down_right + vec_down_left) / 2) - - angle = 30 - height = FreeCAD.ActiveDocument.Site.Terrain.Shape.BoundBox.ZMax - obj.Height.Value - offset = height / math.tan(math.radians(angle)) - - '''cutProfile = Part.makePolygon([vec_sand_left, vec_sand_right, vec_sand_right + FreeCAD.Vector(offset, 0, FreeCAD.ActiveDocument.Site.Terrain.Shape.BoundBox.ZMax), - vec_sand_left + FreeCAD.Vector(-offset, 0, FreeCAD.ActiveDocument.Site.Terrain.Shape.BoundBox.ZMax), vec_sand_left]) - - height = obj.Height.Value - FreeCAD.ActiveDocument.Site.Terrain.Shape.BoundBox.ZMin - offset = height / math.tan(math.radians(angle)) - fillProfile = Part.makePolygon([vec_sand_left, vec_sand_right, vec_sand_right + FreeCAD.Vector(offset, 0, FreeCAD.ActiveDocument.Site.Terrain.Shape.BoundBox.ZMin), - vec_sand_left + FreeCAD.Vector(-offset, 0, FreeCAD.ActiveDocument.Site.Terrain.Shape.BoundBox.ZMin), vec_sand_left]) - - cutshapes, fillshapes = self.makeSolids(obj, [cutProfile, fillProfile], w, (vec_up_right + vec_up_left) / 2) - cuts = self.calculateCut(obj, cutshapes) - fills = self.calculateFill(obj, fillshapes) - if cuts: - for cut in cuts: - Part.show(cut, "RoadCut") - if fills: - for fill in fills: - Part.show(fill, "RoadFill")''' - - obj.Shape = Part.makeCompound(shapes) - - def makeSolids(self, obj, profiles, w, origen): - import Draft - import DraftGeomUtils + n_stations = max(2, int(total_len / interval) + 1) + obj.NumberOfStations = n_stations + # Generar el sólido mediante barrido de secciones shapes = [] - for p in profiles: - if hasattr(p, "CenterOfMass"): - c = p.CenterOfMass - else: - c = p.BoundBox.Center - c = origen - delta = w.Vertexes[0].Point - c - p.translate(delta) + cut_volume = 0 + fill_volume = 0 - if Draft.getType(obj.Base) == "BezCurve": - v1 = obj.Base.Placement.multVec(obj.Base.Points[1]) - w.Vertexes[0].Point - else: - v1 = w.Vertexes[1].Point - w.Vertexes[0].Point - v2 = DraftGeomUtils.getNormal(p) - rot = FreeCAD.Rotation(v2, v1) - #p.rotate(w.Vertexes[0].Point, rot.Axis, math.degrees(rot.Angle)) - ang = rot.toEuler()[0] - p.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), ang) + 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 - if p.Faces: - for f in p.Faces: - sh = w.makePipeShell([f.OuterWire], True, False, 2) - for shw in f.Wires: - if shw.hashCode() != f.OuterWire.hashCode(): - sh2 = w.makePipeShell([shw], True, False, 2) - sh = sh.cut(sh2) - shapes.append(sh) - elif p.Wires: - for pw in p.Wires: - sh = w.makePipeShell([pw], True, False, 2) - shapes.append(sh) - return shapes + sections = self._generate_cross_section(obj, pt, tangent) - def calculateFill(self, obj, solid): - import BOPTools.SplitAPI as splitter - common = solid.common(FreeCAD.ActiveDocument.Site.Terrain.Shape) - if common.Area > 0: - sp = splitter.slice(solid, [common, ], "Split") - common.Placement.Base.z += 1 - solids = [] - for sol in sp.Solids: - common1 = sol.common(common) - if common1.Area > 0: - solids.append(sol) - if len(solids) > 0: - return solids - return None + # 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 - def calculateCut(self, obj, solid): - import BOPTools.SplitAPI as splitter - common = solid.common(FreeCAD.ActiveDocument.Site.Terrain.Shape) - if common.Area > 0: - sp = splitter.slice(solid, [common, ], "Split") - shells = [] - commoncopy = common.copy() - commoncopy.Placement.Base.z -= 1 - for sol in sp.Solids: - common1 = sol.common(commoncopy) - if common1.Area > 0: - shell = sol.Shells[0] - shell = shell.cut(common) - shells.append(shell) - if len(shells) > 0: - return shells - return None + if shapes: + try: + compound = Part.makeCompound(shapes) + obj.Shape = compound - def makeLoft(self, profile): - return + # 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): @@ -318,10 +301,12 @@ class _ViewProviderRoad(ArchComponent.ViewProviderComponent): 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() @@ -329,7 +314,8 @@ class _RoadTaskPanel: self.new = False self.obj = obj - self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantRoad.ui")) + self.form = FreeCADGui.PySideUic.loadUi( + os.path.join(PVPlantResources.__dir__, "PVPlantRoad.ui")) def accept(self): FreeCADGui.Control.closeDialog() @@ -342,275 +328,41 @@ class _RoadTaskPanel: return True -from PySide.QtCore import QT_TRANSLATE_NOOP - -import FreeCAD as App -import FreeCADGui as Gui -import DraftVecUtils -import draftutils.utils as utils -import draftutils.gui_utils as gui_utils -import draftutils.todo as todo -import draftguitools.gui_base_original as gui_base_original -import draftguitools.gui_tool_utils as gui_tool_utils - -from draftutils.messages import _msg -from draftutils.translate import translate - - -class _CommandRoad(gui_base_original.Creator): - """Gui command for the Line tool.""" - - def __init__(self): - # super(_CommandRoad, self).__init__() - gui_base_original.Creator.__init__(self) - self.path = None +# --------------------------------------------------------------------------- +# Comando para dibujar carretera sobre un wire seleccionado +# --------------------------------------------------------------------------- +class _CommandRoad: + """Comando para crear carretera seleccionando un wire + generando alignment.""" def GetResources(self): - """Set icon, menu and tooltip.""" return {'Pixmap': str(os.path.join(DirIcons, "road.svg")), - 'MenuText': QtCore.QT_TRANSLATE_NOOP("PVPlantRoad", "Road"), + 'MenuText': QT_TRANSLATE_NOOP("PVPlantRoad", "Road"), 'Accel': "C, R", - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PVPlantRoad", - "Creates a Road object from setup dialog.")} + 'ToolTip': QT_TRANSLATE_NOOP("PVPlantRoad", + "Crea una carretera con alineamiento profesional.")} - def Activated(self, name=translate("draft", "Line")): - """Execute when the command is called.""" - - gui_base_original.Creator.Activated(self, name=translate("draft", "Line")) - - self.obj = None # stores the temp shape - self.oldWP = None # stores the WP if we modify it + def IsActive(self): + return FreeCAD.ActiveDocument is not None + def Activated(self): sel = FreeCADGui.Selection.getSelection() - - done = False - self.existing = [] - if len(sel) > 0: - print("Crear una carretera a lo largo de un trayecto") - # TODO: chequear que el objeto seleccionado sea un "wire" + wire = None + if sel: import Draft if Draft.getType(sel[0]) == "Wire": - self.path = sel[0] - done = True + wire = sel[0] - if not done: - self.ui.wireUi(name) - self.ui.setTitle("Road") - self.obj = self.doc.addObject("Part::Feature", self.featureName) - gui_utils.format_object(self.obj) - - self.call = self.view.addEventCallback("SoEvent", self.action) - _msg(translate("draft", "Pick first point")) - - def action(self, arg): - """Handle the 3D scene events. - - This is installed as an EventCallback in the Inventor view. - - Parameters - ---------- - arg: dict - Dictionary with strings that indicates the type of event received - from the 3D view. - """ - if arg["Type"] == "SoKeyboardEvent" and arg["Key"] == "ESCAPE": - self.finish() - elif arg["Type"] == "SoLocation2Event": - self.point, ctrlPoint, self.info = gui_tool_utils.getPoint(self, arg) - gui_tool_utils.redraw3DView() - elif (arg["Type"] == "SoMouseButtonEvent" - and arg["State"] == "DOWN" - and arg["Button"] == "BUTTON1"): - if arg["Position"] == self.pos: - return self.finish(False, cont=True) - if (not self.node) and (not self.support): - gui_tool_utils.getSupport(arg) - self.point, ctrlPoint, self.info = gui_tool_utils.getPoint(self, arg) - - if self.point: - self.point = FreeCAD.Vector(self.info["x"], self.info["y"], self.info["z"]) - self.ui.redraw() - self.pos = arg["Position"] - self.node.append(self.point) - self.drawSegment(self.point) - if len(self.node) > 2: - # The wire is closed - if (self.point - self.node[0]).Length < utils.tolerance(): - self.undolast() - if len(self.node) > 2: - self.finish(True, cont=True) - else: - self.finish(False, cont=True) - - def finish(self, closed=False, cont=False): - """Terminate the operation and close the polyline if asked. - - Parameters - ---------- - closed: bool, optional - Close the line if `True`. - """ - self.removeTemporaryObject() - if self.oldWP: - App.DraftWorkingPlane = self.oldWP - if hasattr(Gui, "Snapper"): - Gui.Snapper.setGrid() - Gui.Snapper.restack() - self.oldWP = None - - if len(self.node) > 1: - - if False: - Gui.addModule("Draft") - # The command to run is built as a series of text strings - # to be committed through the `draftutils.todo.ToDo` class. - if (len(self.node) == 2 - and utils.getParam("UsePartPrimitives", False)): - # Insert a Part::Primitive object - p1 = self.node[0] - p2 = self.node[-1] - - _cmd = 'FreeCAD.ActiveDocument.' - _cmd += 'addObject("Part::Line", "Line")' - _cmd_list = ['line = ' + _cmd, - 'line.X1 = ' + str(p1.x), - 'line.Y1 = ' + str(p1.y), - 'line.Z1 = ' + str(p1.z), - 'line.X2 = ' + str(p2.x), - 'line.Y2 = ' + str(p2.y), - 'line.Z2 = ' + str(p2.z), - 'Draft.autogroup(line)', - 'FreeCAD.ActiveDocument.recompute()'] - self.commit(translate("draft", "Create Line"), - _cmd_list) - else: - # Insert a Draft line - rot, sup, pts, fil = self.getStrings() - - _base = DraftVecUtils.toString(self.node[0]) - _cmd = 'Draft.makeWire' - _cmd += '(' - _cmd += 'points, ' - _cmd += 'placement=pl, ' - _cmd += 'closed=' + str(closed) + ', ' - _cmd += 'face=' + fil + ', ' - _cmd += 'support=' + sup - _cmd += ')' - _cmd_list = ['pl = FreeCAD.Placement()', - 'pl.Rotation.Q = ' + rot, - 'pl.Base = ' + _base, - 'points = ' + pts, - 'line = ' + _cmd, - 'Draft.autogroup(line)', - 'FreeCAD.ActiveDocument.recompute()'] - self.commit(translate("draft", "Create Wire"), - _cmd_list) - else: - import Draft - self.path = Draft.makeWire(self.node, closed=False, face=False) - - # super(_CommandRoad, self).finish() - gui_base_original.Creator.finish(self) - if self.ui and self.ui.continueMode: - self.Activated() - - self.makeRoad() - - def makeRoad(self): - makeRoad(self.path) - - def removeTemporaryObject(self): - """Remove temporary object created.""" - if self.obj: - try: - old = self.obj.Name - except ReferenceError: - # object already deleted, for some reason - pass - else: - todo.ToDo.delay(self.doc.removeObject, old) - self.obj = None - - def undolast(self): - """Undoes last line segment.""" - import Part - if len(self.node) > 1: - self.node.pop() - # last = self.node[-1] - if self.obj.Shape.Edges: - edges = self.obj.Shape.Edges - if len(edges) > 1: - newshape = Part.makePolygon(self.node) - self.obj.Shape = newshape - else: - self.obj.ViewObject.hide() - # DNC: report on removal - # _msg(translate("draft", "Removing last point")) - _msg(translate("draft", "Pick next point")) - - def drawSegment(self, point): - """Draws new line segment.""" - import Part - if self.planetrack and self.node: - self.planetrack.set(self.node[-1]) - if len(self.node) == 1: - _msg(translate("draft", "Pick next point")) - elif len(self.node) == 2: - last = self.node[len(self.node) - 2] - newseg = Part.LineSegment(last, point).toShape() - self.obj.Shape = newseg - self.obj.ViewObject.Visibility = True - _msg(translate("draft", "Pick next point")) + 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: - currentshape = self.obj.Shape.copy() - last = self.node[len(self.node) - 2] - if not DraftVecUtils.equals(last, point): - newseg = Part.LineSegment(last, point).toShape() - newshape = currentshape.fuse(newseg) - self.obj.Shape = newshape - _msg(translate("draft", "Pick next point")) - - def wipe(self): - """Remove all previous segments and starts from last point.""" - if len(self.node) > 1: - # self.obj.Shape.nullify() # For some reason this fails - self.obj.ViewObject.Visibility = False - self.node = [self.node[-1]] - if self.planetrack: - self.planetrack.set(self.node[0]) - _msg(translate("draft", "Pick next point")) - - def orientWP(self): - """Orient the working plane.""" - import DraftGeomUtils - if hasattr(App, "DraftWorkingPlane"): - if len(self.node) > 1 and self.obj: - n = DraftGeomUtils.getNormal(self.obj.Shape) - if not n: - n = App.DraftWorkingPlane.axis - p = self.node[-1] - v = self.node[-2].sub(self.node[-1]) - v = v.negative() - if not self.oldWP: - self.oldWP = App.DraftWorkingPlane.copy() - App.DraftWorkingPlane.alignToPointAndAxis(p, n, upvec=v) - if hasattr(Gui, "Snapper"): - Gui.Snapper.setGrid() - Gui.Snapper.restack() - if self.planetrack: - self.planetrack.set(self.node[-1]) - - def numericInput(self, numx, numy, numz): - """Validate the entry fields in the user interface. - - This function is called by the toolbar or taskpanel interface - when valid x, y, and z have been entered in the input fields. - """ - self.point = App.Vector(numx, numy, numz) - self.node.append(self.point) - self.drawSegment(self.point) - self.ui.setNextFocus() + FreeCAD.Console.PrintWarning( + "Selecciona un Wire (polilínea) para usarlo como eje de carretera.\n") if FreeCAD.GuiUp: - FreeCADGui.addCommand('PVPlantRoad', _CommandRoad()) + FreeCADGui.addCommand('PVPlantRoad', _CommandRoad()) \ No newline at end of file