Modelar un muro de contención convencional en Revit es trivial: un muro de sistema, altura fija, geometría ortogonal. Modelar un sistema de paneles MSE Full Height a lo largo de un trazado vial curvo de 400 metros, con alturas variables por topografía, juntas de dilatación de 2 cm y la restricción de que cada panel ocupa exactamente el espacio disponible sin solaparse ni dejar huecos — eso es un problema de geometría computacional que ningún nodo nativo de Dynamo resuelve directamente.
Este artículo documenta el sistema completo que desarrollé para ANTORR BIM: dos scripts Dynamo con ocho nodos Python, aproximadamente 620 líneas de código, y una familia paramétrica con doce parámetros de instancia. El resultado es un flujo de trabajo reproducible que toma una Model Line en Revit como input y produce paneles correctamente posicionados, orientados y parametrizados a lo largo de cualquier trazado recto o curvo.
MSE_Distribucion.dyn y MSE_AjustePaneles.dyn en Autodesk Revit 2026. Distribución automática de paneles Full Height sobre un trazado curvo. (Video en loop / GIF)
Marco teórico: MSE, BIM y diseño computacional
Los muros mecánicamente estabilizados (MSE) son estructuras de contención compuestas por un relleno granular reforzado mediante elementos metálicos o geosintéticos y un revestimiento frontal de paneles prefabricados. Según la Federal Highway Administration [1], estos sistemas constituyen la tecnología geotécnica de contención más ampliamente adoptada en infraestructura vial moderna por su eficiencia constructiva y tolerancia a asentamientos diferenciales.
La tipología Full Height se distingue de los sistemas segmentales (paneles cruciformes apilados) en que cada panel abarca la totalidad de la altura libre del muro en un punto dado. Esta característica introduce una complejidad geométrica específica: las dimensiones del panel no son constantes, sino función del perfil topográfico del terreno. En un trazado recto con talud uniforme, esto se reduce a una interpolación lineal. En un trazado curvo con talud variable, el problema requiere algoritmos de distribución óptima, geometría de cuerda-arco e interpolación multi-tramo.
La norma ISO 19650 [2] establece el marco de gestión de información BIM durante el ciclo de vida del activo construido. En proyectos de infraestructura geotécnica, la aplicación de BIM presenta desafíos derivados de la naturaleza paramétrica de elementos como los paneles MSE. Autodesk Revit no incorpora herramientas nativas para la distribución automática de familias sobre curvas arbitrarias, lo que justifica el desarrollo de soluciones programadas mediante Dynamo y la API de Revit.
Dynamo es el entorno de programación visual integrado en Revit que combina programación nodal con scripts Python a través del motor CPython3. Su acceso a la API transaccional de Revit mediante TransactionManager permite operaciones de creación, transformación y parametrización de elementos que los nodos nativos no cubren. El sistema documentado aquí implementa cuatro algoritmos principales: distribución óptima con paneles de ajuste, interpolación lineal por tramos de alturas, geometría cuerda-arco con tabla de parametrización de 1000 puntos, y junta adaptativa en función de curvatura del trazado.
"El problema de distribuir paneles MSE Full Height en curva no es un problema de Revit — es un problema de geometría computacional. Dynamo y Python son el puente entre el trazado geométrico y el modelo BIM."
Prerrequisitos: familia paramétrica MSE
Antes de ejecutar cualquier script, el modelo Revit debe contener una familia de categoría Modelos Genéricos (Generic Models) con exactamente doce parámetros de instancia de nombres case-sensitive. Estos parámetros son el contrato entre los scripts Dynamo y el modelo — cualquier discrepancia de nombre o tipo de dato silencia la escritura sin generar errores visibles.
| N° | Parámetro | Tipo Dato | Nodo que Escribe | Descripción |
|---|---|---|---|---|
| 1 | Largo | Double (Long.) | D-6.1 / A-3.1 | Ancho horizontal del panel — cuerda en tramos curvos |
| 2 | Altura | Double (Long.) | D-6.2 / A-3.1 | Altura Full Height interpolada por topografía |
| 3 | ID_Tramo | String | D-6.3 | Identificador del tramo: T-01, T-02… |
| 4 | Posicion_EnTramo | Double | D-6.4 | Distancia del centro del panel al inicio del tramo |
| 5 | Total_Paneles | String | D-6.5 | ID del tramo (para schedules y filtros) |
| 6 | EsPanelAjuste | Integer (0/1) | D-6.6 / A-3.1 | 1 = panel de ajuste, 0 = panel estándar |
| 7 | Angulo_Rotacion | Double | D-6.7 | Ángulo tangente al trazado en la posición del panel |
| 8 | Bloqueado | Integer (0/1) | D-6.8 | Flag de bloqueo para edición manual posterior |
| 9 | Viga Corona | Integer (0/1) | D-6.9 | Número secuencial del panel en el tramo |
| 10 | Coord_X | Double (Long.) | D-6.10 | Coordenada X absoluta (sistema Revit en pies) |
| 11 | Coord_Y | Double (Long.) | D-6.10 | Coordenada Y absoluta |
| 12 | Radio_Curva | Double (Long.) | D-6.10 | Radio de curvatura del trazado en la posición del panel |
Convención de prefijos: Los nodos del script MSE_Distribucion se identifican con prefijo D- (ej: D-3.1 Distribucion) y los del script MSE_AjustePaneles con A- (ej: A-3.1 AjusteCuerda), evitando ambigüedad al referenciar nodos entre scripts.
Configuración del entorno
Siete ajustes previos determinan si los scripts funcionarán correctamente. El más crítico es Geometry Scaling: Extra Large — sin él, Dynamo trunca las coordenadas de trazados superiores a 100 metros y los paneles aparecen desplazados varios kilómetros. El segundo en importancia es limpiar el Element Binding antes de cada nueva ejecución; sin este paso, una re-ejecución del script sobre el mismo tramo mueve los paneles de otros tramos ya colocados.
| N° | Configuración | Valor | Ubicación | Justificación |
|---|---|---|---|---|
| 1 | Geometry Scaling | Extra Large | Dynamo → Settings | Trazados >100 m producen truncamiento sin esta config |
| 2 | Element Binding | Limpiar por tramo | Dynamo → Settings | Sin limpieza, re-ejecuciones afectan tramos anteriores |
| 3 | Motor Python | CPython3 | Nodo Python → Engine | Scripts usan clr.AddReference para API de Revit |
| 4 | Modo Ejecución | Manual | Barra Dynamo | Evita ejecución involuntaria al modificar parámetros |
| 5 | Trazado | 1 Model Line / tramo | Revit → Model Line | No usar Detail Line — no tiene geometría accesible desde API |
| 6 | Nivel Base | Menor elevación | Revit → Levels | D-5.1 Colocar selecciona el nivel más bajo automáticamente |
| 7 | Familia MSE | Cargada en proyecto | Revit → Load Family | Debe aparecer en el selector D-1.6 Familia |
Script MSE_Distribucion.dyn — 61 nodos, 8 grupos
MSE_Distribucion es el script principal. Toma una Model Line como trazado y produce instancias de la familia MSE posicionadas, orientadas y parametrizadas a lo largo de toda su longitud. Los 61 nodos se organizan en ocho grupos visuales en el canvas de Dynamo, cada uno con una responsabilidad específica y un color de fondo diferenciador.
Doce nodos configurables por el usuario, los únicos que requieren intervención manual antes de ejecutar el script. En Dynamo Player aparecen como campos editables. El nodo más relevante es D-1.10 ParseInflexion, el único nodo Python del grupo: convierte dos cadenas CSV (distancias y alturas de inflexión topográfica) en listas numéricas que alimentan el algoritmo de interpolación de alturas.
D-1.1
Trazado
Select Model ElementD-1.2
Altura
Number SliderD-1.3
Junta
Number SliderD-1.4
PanelAlInicio
BooleanD-1.5
IDTramo
String InputD-1.6
Familia
Family TypesD-1.7
Ancho
Number SliderD-1.8
DistInflexion
String InputD-1.9
AltInflexion
String InputD-1.10
ParseInflexion
Python Script · CPython3D-1.11
DistLista
List.GetItemAtIndexD-1.12
AltLista
List.GetItemAtIndexTres nodos que convierten el elemento modelo seleccionado en datos geométricos utilizables: la curva Dynamo y su longitud en metros. La longitud alimenta simultáneamente al algoritmo de distribución (D-3.1), al algoritmo de alturas (D-3B) y al nodo Watch informativo.
D-2.1
Curva
CurveElement.CurveD-2.2
Longitud
Curve.LengthD-CB
Code Block
Code Block
Un único nodo Python de 70 líneas que contiene la lógica de distribución. Calcula N_std = floor((L + junta) / (ancho + junta)), determina si existe residuo, y decide si colocar 0, 1 o 2 paneles de ajuste. El panel de ajuste nunca es menor que AMIN = 0.50 m y su ancho se redondea a múltiplos de STEP = 0.05 m. La salida es una lista de tres sublistas: [anchos, N_total, es_ajuste].
D-E1
Anchos
List.GetItemAtIndexD-E2
NTotal
List.GetItemAtIndexD-E3
EsAjuste
List.GetItemAtIndex
El nodo D-3B interpola la altura de cada panel entre los puntos de inflexión topográfica definidos por el usuario en D-1.8/D-1.9. El algoritmo calcula el centro acumulado de cada panel, localiza el segmento de inflexión correspondiente, interpola linealmente y aplica un suavizado: la diferencia máxima de altura entre paneles consecutivos es de ±5 cm (STEP = 0.05 m), lo que garantiza transiciones constructivamente viables entre paneles de distintas alturas.
D-3B
Alturas
Python Script · CPython3
D-4.1 CoordenadasPanel itera sobre la lista de anchos, calcula el punto de inserción de cada panel como PointAtSegmentLength(centro_acumulado) y el ángulo tangente como atan2(t.Y, t.X) donde t es el vector tangente en ese punto. Si la API de Revit no puede calcular la tangente (casos degenerados en geometría compleja), un fallback por diferencia finita entre dos puntos cercanos garantiza un ángulo de rotación razonable.
D-4.1
CoordenadasPanel
Python Script · CPython3D-4.E1 / D-4.E2 / D-4.E3
Extractores G4
List.GetItemAtIndex
D-5.1 Colocar es el único nodo que escribe geometría en el modelo. Selecciona automáticamente el nivel de menor elevación del proyecto, convierte las coordenadas Dynamo (metros) a pies con el factor FT = 3.28083989501312, y crea cada instancia con doc.Create.NewFamilyInstance(xyz, fam_type, level, StructuralType.NonStructural) dentro de una transacción. La salida es la lista de instancias, que se distribuye simultáneamente hacia todos los nodos del Grupo 6 y el nodo D-7.1 Rotar.
D-5.1
Colocar
Python Script · CPython3
Diez nodos que escriben los doce parámetros de la familia. Los nueve primeros son nodos nativos Element.SetParameterByName; cada uno requiere un nodo String auxiliar con el nombre exacto del parámetro. El nodo D-6.10 Coordenadas es un script Python que escribe Coord_X, Coord_Y (desde Location.Point) y Radio_Curva (desde 1.0 / curva.CurvatureAtParameter(0.5)) en una sola transacción, devolviendo las instancias como passthrough para mantener la cadena de dependencias.
D-6.1
Largo
SetParameterByNameD-6.2
Altura
SetParameterByNameD-6.3
ID_Tramo
SetParameterByNameD-6.4
Posicion_EnTramo
SetParameterByNameD-6.5
Total_Paneles
SetParameterByNameD-6.6
EsPanelAjuste
SetParameterByNameD-6.7
Angulo_Rotacion
SetParameterByNameD-6.8
Bloqueado
SetParameterByNameD-6.9
Viga Corona
SetParameterByNameD-6.10
Coordenadas
Python Script · CPython3Automatización BIM avanzada
¿Tu proyecto de infraestructura requiere automatización paramétrica en Revit?
Desarrollamos scripts Dynamo y familias paramétricas a medida para proyectos de infraestructura vial, geotecnia y obras civiles. Desde distribución de elementos sobre trazados hasta extracción automática de cantidades por tramo.
D-7.1 Rotar aplica una rotación absoluta, no relativa. El algoritmo calcula ang_diff = radians(deseado) - loc.Rotation, normaliza a [−π, π] y llama a ElementTransformUtils.RotateElement solo si |ang_diff| > 0.001 radianes. El doc.Regenerate() previo al bucle es crítico: sin él, el nodo lee posiciones pre-transacción y los ángulos calculados con las coordenadas reales de D-5.1 resultan inconsistentes.
D-7.1
Rotar
Python Script · CPython3Script MSE_AjustePaneles.dyn — Ajuste cuerda-arco
MSE_AjustePaneles es el script de corrección. Se ejecuta después de MSE_Distribucion, sobre un subconjunto de paneles seleccionados manualmente que requieren recalculación por geometría de curva. Su núcleo es el nodo A-3.1 AjusteCuerda: 328 líneas Python que implementan el ajuste completo en cinco fases.
Largo del panel MSE debe corresponder a la longitud de cuerda (distancia entre extremos en planta), no a la longitud de arco, para que el panel físico cierre correctamente contra sus vecinos.
El problema que AjustePaneles resuelve: cuando MSE_Distribucion coloca paneles en una curva, el parámetro Largo se calcula sobre la longitud de arco del trazado. Pero un panel físico es un elemento recto — su ancho real en planta es la longitud de cuerda entre sus dos extremos, que es menor que el arco correspondiente. En curvas de radio pequeño (R < 50 m) la diferencia supera el milímetro constructivo y los paneles se solapan visualmente en el modelo.
Las cinco fases del algoritmo A-3.1:
atan2 de vectores tangentes) y estima la junta necesaria entre 2.0 cm (rectas) y 2.5 cm (curvas cerradas) proporcional al cambio de ángulo por panel.PanelPrevio) y el posterior (PanelSiguiente), con suavizado de ±5 cm entre paneles consecutivos.ri) y final (rf) del arco, obtiene el centroide geométrico, calcula la longitud de cuerda como distancia Euclidiana ri→rf, y asigna ese valor (no el arco) al parámetro Largo. El último panel usa la longitud exacta restante sin redondeo, cerrando perfectamente el segmento.A-2.1
Paneles
Select Model ElementsA-2.2
Trazado
Select Model ElementA-2.3
LargoObjetivo
Number SliderA-2.4
PanelAjusteAlInicio
BooleanA-2.5
PanelPrevio
Select Model ElementA-2.6
PanelSiguiente
Select Model ElementA-3.1
AjusteCuerda
Python Script · CPython3A-W
Resultado
WatchCódigo fuente Python — Fichas técnicas
El procedimiento para todos los nodos es idéntico: doble clic sobre el nodo Python para abrir el editor, Ctrl+A para seleccionar el código por defecto, Delete para borrarlo, pegar el código de la ficha correspondiente y cerrar el editor.
float. Sin dependencias externas.
Ver código fuente
dist = [float(x.strip()) for x in IN[0].split(",")]
alt = [float(x.strip()) for x in IN[1].split(",")]
OUT = [dist, alt]
Ver código fuente — 70 líneas
import math
L = IN[0]; junta = IN[1]
ref = "Inicio" if IN[2] else "Final"
ANCHO = IN[3]; AMIN = 0.50; STEP = 0.05
modulo = ANCHO + junta
N_std = int(math.floor((L + junta) / modulo))
if N_std < 1: N_std = 1
ocupado = N_std * ANCHO + (N_std - 1) * junta
residuo = L - ocupado
if abs(residuo) < 0.001:
anchos = [ANCHO] * N_std
es_ajuste = [False] * N_std
elif residuo > 0:
ancho_especial = residuo - junta
if ancho_especial > ANCHO:
N_std += 1
ocupado = N_std * ANCHO + (N_std - 1) * junta
residuo = L - ocupado
ancho_especial = residuo - junta
if ancho_especial >= AMIN and ancho_especial <= ANCHO:
if ref == "Final":
anchos = [ANCHO] * N_std + [ancho_especial]
es_ajuste = [False] * N_std + [True]
else:
anchos = [ancho_especial] + [ANCHO] * N_std
es_ajuste = [True] + [False] * N_std
elif ancho_especial > 0.001:
N_std -= 1
espacio = L - N_std * ANCHO - N_std * junta
p1 = round(round(espacio / 2.0 / STEP) * STEP, 4)
p2 = round(espacio - p1 - junta, 4)
if p1 < AMIN: p1 = AMIN; p2 = round(espacio - p1 - junta, 4)
if p2 < AMIN: p2 = AMIN; p1 = round(espacio - p2 - junta, 4)
if p1 >= AMIN and p2 >= AMIN and p1 <= ANCHO and p2 <= ANCHO:
if ref == "Final":
anchos = [ANCHO] * N_std + [p1, p2]
es_ajuste = [False] * N_std + [True, True]
else:
anchos = [p1, p2] + [ANCHO] * N_std
es_ajuste = [True, True] + [False] * N_std
else:
anchos = [ANCHO] * (N_std + 1); es_ajuste = [False] * (N_std + 1)
else:
anchos = [ANCHO] * N_std; es_ajuste = [False] * N_std
else:
anchos = [ANCHO] * N_std; es_ajuste = [False] * N_std
N_total = len(anchos)
OUT = [anchos, N_total, es_ajuste]
Ver código fuente — 52 líneas
import math
anchos = IN[0]; junta = IN[1]
dist_infl = list(IN[2]); alt_infl = list(IN[3])
L = IN[4]; alt_default = IN[5]
HMIN = 0.10; STEP = 0.05
dist_infl[-1] = L
if len(dist_infl) == 2 and abs(alt_infl[0] - alt_infl[1]) < 0.001:
alturas = [round(round(alt_default / STEP) * STEP, 4)] * len(anchos)
else:
centros = []; acum = 0.0
for a in anchos:
centros.append(acum + a / 2.0)
acum = acum + a + junta
alturas_obj = []
for centro in centros:
if centro <= dist_infl[0]: h = alt_infl[0]
elif centro >= dist_infl[-1]: h = alt_infl[-1]
else:
for j in range(len(dist_infl) - 1):
if dist_infl[j] <= centro <= dist_infl[j+1]:
t = (centro - dist_infl[j]) / (dist_infl[j+1] - dist_infl[j])
h = alt_infl[j] + t * (alt_infl[j+1] - alt_infl[j])
break
h_round = round(h / STEP) * STEP
if h_round < HMIN: h_round = HMIN
alturas_obj.append(round(h_round, 4))
alturas = [alturas_obj[0]]
for i in range(1, len(alturas_obj)):
h_prev = alturas[i-1]; h_obj = alturas_obj[i]
diff = h_obj - h_prev
if abs(diff) <= STEP + 0.001: alturas.append(h_obj)
elif diff > 0: alturas.append(round(h_prev + STEP, 4))
else:
nueva = round(h_prev - STEP, 4)
if nueva < HMIN: nueva = HMIN
alturas.append(nueva)
OUT = alturas
Ver código fuente — 43 líneas
import math
anchos = IN[0]; junta = IN[1]; curva = IN[2]
puntos = []; distancias = []; angulos = []
acum = 0.0; L = curva.Length
for i in range(len(anchos)):
a = anchos[i]
centro = max(acum + a / 2.0, 0.001)
centro = min(centro, L - 0.001)
distancias.append(centro)
try: pt = curva.PointAtSegmentLength(centro); puntos.append(pt)
except: puntos.append(curva.StartPoint)
try:
t = curva.TangentAtSegmentLength(centro)
angulos.append(math.degrees(math.atan2(t.Y, t.X)))
except:
s0 = max(acum, L * 0.001); s1 = min(acum + a, L * 0.999)
try:
p0 = curva.PointAtSegmentLength(s0)
p1 = curva.PointAtSegmentLength(s1)
angulos.append(math.degrees(math.atan2(p1.Y - p0.Y, p1.X - p0.X)))
except: angulos.append(0.0)
acum += a
if i < len(anchos) - 1: acum += junta
OUT = [puntos, distancias, angulos]
Ver código fuente — 37 líneas
import clr
clr.AddReference('RevitAPI'); clr.AddReference('RevitServices')
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Structure import StructuralType
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument
fam_type = UnwrapElement(IN[0])
puntos = IN[1] if isinstance(IN[1], list) else [IN[1]]
FT = 3.28083989501312
levels = FilteredElementCollector(doc).OfClass(Level).ToElements()
level = sorted(levels, key=lambda lv: lv.Elevation)[0]
z = level.Elevation
TransactionManager.Instance.EnsureInTransaction(doc)
instancias = []
for pt in puntos:
if pt is None: instancias.append(None); continue
try:
xyz = XYZ(pt.X * FT, pt.Y * FT, z)
inst = doc.Create.NewFamilyInstance(xyz, fam_type, level, StructuralType.NonStructural)
instancias.append(inst)
except: instancias.append(None)
TransactionManager.Instance.TransactionTaskDone()
clr.AddReference('RevitNodes')
import Revit; clr.ImportExtensions(Revit.Elements)
OUT = [inst.ToDSType(True) if inst is not None else None for inst in instancias]
Ver código fuente — 33 líneas
import clr
clr.AddReference('RevitAPI'); clr.AddReference('RevitServices')
from Autodesk.Revit.DB import *
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument
elems = UnwrapElement(IN[0]); curva = IN[1]
if not isinstance(elems, list): elems = [elems]
TransactionManager.Instance.EnsureInTransaction(doc)
for elem in elems:
loc = elem.Location
if loc is None: continue
pt = loc.Point
x_param = elem.LookupParameter("Coord_X")
y_param = elem.LookupParameter("Coord_Y")
r_param = elem.LookupParameter("Radio_Curva")
if x_param: x_param.Set(pt.X)
if y_param: y_param.Set(pt.Y)
if r_param:
try: r_param.Set(1.0 / curva.CurvatureAtParameter(0.5))
except: r_param.Set(0.0)
TransactionManager.Instance.TransactionTaskDone()
OUT = IN[0]
Ver código fuente — 52 líneas
import clr, math
clr.AddReference('RevitAPI'); clr.AddReference('RevitServices')
from Autodesk.Revit.DB import *
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument
elems = UnwrapElement(IN[0]); angulos = IN[1]
if not isinstance(elems, list): elems = [elems]
if not isinstance(angulos, list): angulos = [angulos]
resultados = []
TransactionManager.Instance.EnsureInTransaction(doc)
doc.Regenerate()
for i in range(len(elems)):
elem = elems[i]
if elem is None: resultados.append(None); continue
if i >= len(angulos): resultados.append(elem); continue
ang_deseado = angulos[i]
try:
loc = elem.Location
if loc is None: resultados.append(elem); continue
pt = loc.Point
axis = Line.CreateBound(XYZ(pt.X, pt.Y, pt.Z), XYZ(pt.X, pt.Y, pt.Z + 1.0))
ang_actual = loc.Rotation
ang_diff = math.radians(ang_deseado) - ang_actual
while ang_diff > math.pi: ang_diff -= 2 * math.pi
while ang_diff < -math.pi: ang_diff += 2 * math.pi
if abs(ang_diff) > 0.001:
ElementTransformUtils.RotateElement(doc, elem.Id, axis, ang_diff)
resultados.append(elem)
except: resultados.append(elem)
TransactionManager.Instance.TransactionTaskDone()
OUT = resultados
Largo = cuerda (no arco), último panel sin snap. Constantes: FT=3.28084, STEP_M=0.05, AMIN_M=0.50, JUNTA_MIN=0.020, JUNTA_MAX=0.025, N_FULL=1000, HSTEP=0.05.
Ver código fuente — 328 líneas
import clr, math
clr.AddReference('RevitAPI')
clr.AddReference('RevitServices')
clr.AddReference('RevitNodes')
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Structure import StructuralType
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
import Revit
clr.ImportExtensions(Revit.Elements)
doc = DocumentManager.Instance.CurrentDBDocument
paneles_dy = IN[0] if isinstance(IN[0], list) else [IN[0]]
curva_elem = UnwrapElement(IN[1])
largo_obj_m = float(IN[2])
ajuste_inicio = bool(IN[3])
panel_prev_dy = IN[4]
panel_next_dy = IN[5]
FT = 3.28083989501312
MI = 1.0 / FT
STEP_M = 0.05
AMIN_M = 0.50
JUNTA_MIN_M = 0.020
JUNTA_MAX_M = 0.025
def snap(v):
return round(round(v / STEP_M) * STEP_M, 4)
def copy_params(src, dst):
for pname in ['Largo','Altura','ID_Tramo','Total_Paneles',
'EsPanelAjuste','Angulo_Rotacion','Bloqueado','Viga Corona']:
try:
ps = src.LookupParameter(pname)
pd = dst.LookupParameter(pname)
if ps and pd and not pd.IsReadOnly:
st = ps.StorageType
if st == StorageType.Double: pd.Set(ps.AsDouble())
elif st == StorageType.Integer: pd.Set(ps.AsInteger())
elif st == StorageType.String: pd.Set(ps.AsString() or '')
except:
pass
panel_prev = UnwrapElement(panel_prev_dy) if panel_prev_dy else None
panel_next = UnwrapElement(panel_next_dy) if panel_next_dy else None
def get_altura_m(elem):
p = elem.LookupParameter('Altura')
return (p.AsDouble() * MI) if p else None
paneles_rv = [UnwrapElement(p) for p in paneles_dy if p is not None]
revit_curve = curva_elem.GeometryCurve
L_ft_tot = revit_curve.Length
p0_raw = revit_curve.GetEndParameter(0)
p1_raw = revit_curve.GetEndParameter(1)
p_range = p1_raw - p0_raw
def to_norm(p_raw):
return max(0.0, min(1.0, (p_raw - p0_raw) / p_range))
def eval_norm(t):
return revit_curve.Evaluate(max(0.00001, min(0.99999, t)), True)
def panel_t(elem):
try:
return to_norm(revit_curve.Project(elem.Location.Point).Parameter)
except:
return 0.0
def get_largo_ft(elem):
p = elem.LookupParameter('Largo')
return p.AsDouble() if p else largo_obj_m * FT
def get_z(elem):
try:
return elem.Location.Point.Z
except:
return 0.0
N_FULL = 1000
ts_full = [k / float(N_FULL) for k in range(N_FULL + 1)]
pts_full = [eval_norm(t) for t in ts_full]
arc_full = [0.0]
for k in range(1, N_FULL + 1):
dx = pts_full[k].X - pts_full[k-1].X
dy = pts_full[k].Y - pts_full[k-1].Y
arc_full.append(arc_full[-1] + math.sqrt(dx*dx + dy*dy))
L_curve_ft = arc_full[-1]
JUNTA_M = JUNTA_MIN_M
JUNTA_FT = JUNTA_M * FT
def t_to_arc(t_val):
t_val = max(0.0, min(1.0, t_val))
lo, hi = 0, N_FULL
while lo < hi - 1:
mid = (lo + hi) // 2
if ts_full[mid] <= t_val: lo = mid
else: hi = mid
if ts_full[hi] == ts_full[lo]: return arc_full[lo]
frac = (t_val - ts_full[lo]) / (ts_full[hi] - ts_full[lo])
return arc_full[lo] + frac * (arc_full[hi] - arc_full[lo])
def arc_to_t(s_ft):
s_ft = max(0.0, min(L_curve_ft, s_ft))
lo, hi = 0, N_FULL
while lo < hi - 1:
mid = (lo + hi) // 2
if arc_full[mid] <= s_ft: lo = mid
else: hi = mid
if arc_full[hi] == arc_full[lo]: return ts_full[lo]
frac = (s_ft - arc_full[lo]) / (arc_full[hi] - arc_full[lo])
return ts_full[lo] + frac * (ts_full[hi] - ts_full[lo])
def rv_at_arc(s_ft):
return eval_norm(arc_to_t(s_ft))
def panel_arc(elem):
return t_to_arc(panel_t(elem))
paneles_rv.sort(key=panel_arc)
N_exist = len(paneles_rv)
template = paneles_rv[0]
z_nivel = get_z(template)
s_c0 = panel_arc(paneles_rv[0])
s_cN = panel_arc(paneles_rv[-1])
s_seg_ini = s_c0 - get_largo_ft(paneles_rv[0]) / 2.0
s_seg_fin = s_cN + get_largo_ft(paneles_rv[-1]) / 2.0
if s_seg_fin - s_seg_ini < largo_obj_m * FT * 0.5:
OUT = ['ERROR: segmento demasiado pequeno (' + str(round((s_seg_fin-s_seg_ini)*MI,3)) + 'm). Verifica la seleccion de paneles y el trazado.']
else:
L_seg_ft = s_seg_fin - s_seg_ini
L_seg_m = L_seg_ft * MI
try:
tang_ini = eval_norm(arc_to_t(s_seg_ini + 0.001))
tang_mid = eval_norm(arc_to_t(s_seg_ini + L_seg_ft * 0.5))
tang_fin = eval_norm(arc_to_t(s_seg_fin - 0.001))
pt_ini = rv_at_arc(s_seg_ini)
pt_fin = rv_at_arc(s_seg_fin)
dx1 = tang_mid.X - tang_ini.X; dy1 = tang_mid.Y - tang_ini.Y
dx2 = tang_fin.X - tang_mid.X; dy2 = tang_fin.Y - tang_mid.Y
ang1 = math.degrees(math.atan2(dy1, dx1))
ang2 = math.degrees(math.atan2(dy2, dx2))
n_est = max(1, L_seg_m / (largo_obj_m + JUNTA_MIN_M))
delta_ang_total = abs(ang2 - ang1)
delta_ang_per_panel = delta_ang_total / n_est
factor = min(1.0, delta_ang_per_panel / 10.0)
JUNTA_M = JUNTA_MIN_M + factor * (JUNTA_MAX_M - JUNTA_MIN_M)
JUNTA_FT = JUNTA_M * FT
except:
JUNTA_M = JUNTA_MIN_M
JUNTA_FT = JUNTA_M * FT
def rv_at_local(s_local_ft):
return rv_at_arc(s_seg_ini + max(0.0, min(L_seg_ft, s_local_ft)))
modulo = largo_obj_m + JUNTA_M
N_std = int(math.floor((L_seg_m + JUNTA_M) / modulo))
if N_std < 1: N_std = 1
ocupado = N_std * largo_obj_m + (N_std - 1) * JUNTA_M
residuo = L_seg_m - ocupado
ancho_ult = residuo - JUNTA_M
if abs(residuo) < 0.001:
anchos = [largo_obj_m] * N_std
es_ajuste = [False] * N_std
elif ancho_ult >= AMIN_M and ancho_ult <= largo_obj_m:
if ajuste_inicio:
anchos = [snap(ancho_ult)] + [largo_obj_m] * N_std
es_ajuste = [True] + [False] * N_std
else:
anchos = [largo_obj_m] * N_std + [snap(ancho_ult)]
es_ajuste = [False] * N_std + [True]
elif ancho_ult > 0.001:
N2 = N_std - 1
esp = L_seg_m - N2 * largo_obj_m - N2 * JUNTA_M
p1v = snap(esp / 2.0)
p2v = snap(esp - p1v - JUNTA_M)
if p1v >= AMIN_M and p2v >= AMIN_M:
if ajuste_inicio:
anchos = [p1v, p2v] + [largo_obj_m] * N2
es_ajuste = [True, True] + [False] * N2
else:
anchos = [largo_obj_m] * N2 + [p1v, p2v]
es_ajuste = [False] * N2 + [True, True]
else:
anchos = [largo_obj_m] * N_std
es_ajuste = [False] * N_std
else:
anchos = [largo_obj_m] * N_std
es_ajuste = [False] * N_std
N_nuevo = len(anchos)
nuevos_creados = 0
eliminados = 0
fam_type = template.Symbol
TransactionManager.Instance.EnsureInTransaction(doc)
doc.Regenerate()
if N_nuevo > N_exist:
levels = FilteredElementCollector(doc).OfClass(Level).ToElements()
level = sorted(levels, key=lambda lv: lv.Elevation)[0]
pt_mid = rv_at_arc(s_seg_ini + L_seg_ft / 2.0)
pt_tmp = XYZ(pt_mid.X, pt_mid.Y, z_nivel)
for _ in range(N_nuevo - N_exist):
inst = doc.Create.NewFamilyInstance(pt_tmp, fam_type, level, StructuralType.NonStructural)
copy_params(template, inst)
paneles_rv.append(inst)
nuevos_creados += 1
elif N_nuevo < N_exist:
sobrantes = paneles_rv[N_nuevo:]
eliminados = len(sobrantes)
for e in sobrantes:
doc.Delete(e.Id)
paneles_rv = paneles_rv[:N_nuevo]
doc.Regenerate()
h_ini = get_altura_m(panel_prev) if panel_prev else get_altura_m(paneles_rv[0])
h_fin = get_altura_m(panel_next) if panel_next else get_altura_m(paneles_rv[-1])
if h_ini is None: h_ini = get_altura_m(paneles_rv[0])
if h_fin is None: h_fin = get_altura_m(paneles_rv[-1])
if h_ini is None: h_ini = 2.4
if h_fin is None: h_fin = h_ini
HSTEP = 0.05
def snap_h(v):
return round(round(v / HSTEP) * HSTEP, 4)
N = N_nuevo
alturas_obj = []
for idx in range(N):
t_h = idx / max(1, N - 1)
h = h_ini + t_h * (h_fin - h_ini)
alturas_obj.append(snap_h(h))
alturas_final = [alturas_obj[0]]
for idx in range(1, N):
diff = alturas_obj[idx] - alturas_final[-1]
if abs(diff) <= HSTEP + 0.001:
alturas_final.append(alturas_obj[idx])
elif diff > 0:
alturas_final.append(round(alturas_final[-1] + HSTEP, 4))
else:
alturas_final.append(round(alturas_final[-1] - HSTEP, 4))
resultados = []
acum_ft = 0.0
acum_check_ft = sum(a * FT + JUNTA_FT for a in anchos) - JUNTA_FT
last_exact_ft = L_seg_ft - (acum_check_ft - anchos[-1] * FT)
for i, elem in enumerate(paneles_rv):
if i == len(paneles_rv) - 1:
a_ft = max(AMIN_M * FT, last_exact_ft)
else:
a_ft = anchos[i] * FT
ri = rv_at_local(acum_ft)
rf = rv_at_local(acum_ft + a_ft)
cx = (ri.X + rf.X) / 2.0
cy = (ri.Y + rf.Y) / 2.0
dx_m = (rf.X - ri.X) * MI
dy_m = (rf.Y - ri.Y) * MI
cuerda_m = math.sqrt(dx_m*dx_m + dy_m*dy_m)
ang_deg = math.degrees(math.atan2(dy_m, dx_m))
try:
loc = elem.Location
pt = loc.Point
delta = XYZ(cx - pt.X, cy - pt.Y, z_nivel - pt.Z)
if delta.GetLength() > 0.00001:
ElementTransformUtils.MoveElement(doc, elem.Id, delta)
lp = elem.LookupParameter('Largo')
if lp:
largo_set = cuerda_m if i == len(paneles_rv)-1 else snap(cuerda_m)
lp.Set(largo_set * FT)
lpa = elem.LookupParameter('EsPanelAjuste')
if lpa and not lpa.IsReadOnly: lpa.Set(1 if es_ajuste[i] else 0)
lh = elem.LookupParameter('Altura')
if lh and not lh.IsReadOnly and i < len(alturas_final):
lh.Set(alturas_final[i] * FT)
doc.Regenerate()
loc2 = elem.Location
pt2 = loc2.Point
axis = Line.CreateBound(pt2, XYZ(pt2.X, pt2.Y, pt2.Z + 1.0))
ang_diff = math.radians(ang_deg) - loc2.Rotation
while ang_diff > math.pi: ang_diff -= 2 * math.pi
while ang_diff < -math.pi: ang_diff += 2 * math.pi
if abs(ang_diff) > 0.0005:
ElementTransformUtils.RotateElement(doc, elem.Id, axis, ang_diff)
tag = ' [AJUSTE]' if es_ajuste[i] else ''
h_str = str(alturas_final[i]) + 'm' if i < len(alturas_final) else ''
resultados.append('P' + str(i) + tag + ': ' + str(snap(cuerda_m)) + 'm ' + str(round(ang_deg,1)) + 'deg h=' + h_str)
except Exception as ex:
resultados.append('P' + str(i) + ' ERROR: ' + str(ex)[:80])
acum_ft += a_ft + JUNTA_FT
TransactionManager.Instance.TransactionTaskDone()
resumen = ('Seg: ' + str(round(L_seg_m,3)) + 'm | '
+ str(N_nuevo) + ' paneles (+' + str(nuevos_creados)
+ ' nuevos, -' + str(eliminados) + ' elim) | '
+ str(largo_obj_m) + 'm | Ajuste al '
+ ('Inicio' if ajuste_inicio else 'Final'))
OUT = [resumen] + resultados
Implementación en Dynamo Player — Flujo de trabajo
El flujo de trabajo completo para un tramo de muro MSE sigue seis pasos secuenciales. El paso 2 — limpiar el Element Binding — es el que más se omite y el que más problemas genera. Sin él, la segunda ejecución del script moverá los paneles del primer tramo ya colocado.
Referencia de constantes
Las siguientes constantes están embebidas en los scripts y no son configurables desde Dynamo Player. Para modificarlas es necesario editar el código fuente en el editor del nodo Python correspondiente.
| Constante | Valor | Nodo ID | Script | Descripción |
|---|---|---|---|---|
| AMIN | 0.50 m | D-3.1 | Distribución | Ancho mínimo de panel de ajuste |
| STEP | 0.05 m | D-3.1 | Distribución | Redondeo de anchos a múltiplos de 5 cm |
| HMIN | 0.10 m | D-3B | Distribución | Altura mínima de panel |
| FT | 3.28083989501312 | D-5.1, D-7.1, A-3.1 | Ambos | Factor metros → pies (unidad interna Revit) |
| STEP_M | 0.05 m | A-3.1 | Ajuste | Redondeo anchos en AjusteCuerda |
| AMIN_M | 0.50 m | A-3.1 | Ajuste | Ancho mínimo en AjusteCuerda |
| JUNTA_MIN_M | 0.020 m | A-3.1 | Ajuste | Junta mínima (trazados rectos) |
| JUNTA_MAX_M | 0.025 m | A-3.1 | Ajuste | Junta máxima (curvas cerradas) |
| N_FULL | 1000 | A-3.1 | Ajuste | Puntos en tabla arco-parámetro |
| HSTEP | 0.05 m | A-3.1 | Ajuste | Redondeo alturas en AjusteCuerda |
Solución de problemas
| Síntoma | Causa probable | Solución |
|---|---|---|
| Paneles no aparecen en el modelo | Element Binding anterior activo | Limpiar Element Binding y re-ejecutar el script |
| Paneles truncados o desplazados kilómetros | Geometry Scaling insuficiente | Dynamo Settings → Geometry Scaling: Extra Large |
| Error en D-5.1 al ejecutar | Familia no cargada en el proyecto | Verificar que la familia MSE aparece seleccionada en D-1.6 Familia |
| Todas las alturas son iguales | Mismo valor en todos los puntos de inflexión | Verificar D-1.9 AltInflexion: los valores deben ser distintos para activar la interpolación |
| Panel de ajuste en lado equivocado | PanelAlInicio con valor incorrecto | Invertir el booleano D-1.4 (Distribución) o A-2.4 (Ajuste) |
| A-3.1 reporta "ERROR: segmento demasiado pequeño" | Selección de paneles incompleta | Seleccionar TODOS los paneles del segmento curvo antes de ejecutar |
| Rotaciones erróneas en varios paneles | Tangente indefinida en geometría compleja | Fallback automático en D-4.1 — verificar geometría del trazado |
| Parámetros no se escriben (sin error) | Nombre de parámetro con mayúscula incorrecta | Los nombres son case-sensitive. Verificar exactitud: "Coord_X" ≠ "coord_x" |
Calculadora de Distribución de Paneles MSE
Configura la longitud del trazado, ancho estándar y junta para obtener la distribución óptima de paneles. Replica el algoritmo D-3.1 documentado en este artículo.
* Replica el algoritmo D-3.1 Distribucion con constantes AMIN=0.50 m y STEP=0.05 m. Los resultados son idénticos a la ejecución del nodo Python en Dynamo.
Cómo citar este artículo
APA 7.ª edición
Torres, E. (2026, abril 1). MSE Full Height en Revit 2026: distribución y ajuste de paneles con Dynamo y Python. ANTORR Ingeniería S.A.S. https://antorr.co/academia/mse-full-height-dynamo-revit.html
Chicago 17.ª edición
Torres, Emil. "MSE Full Height en Revit 2026: distribución y ajuste de paneles con Dynamo y Python." ANTORR Academia, 1 de abril de 2026. https://antorr.co/academia/mse-full-height-dynamo-revit.html
Referencias bibliográficas y técnicas
- Torres, E. (2026, abril 1). MSE Full Height en Revit 2026: distribución y ajuste de paneles con Dynamo y Python. ANTORR Ingeniería S.A.S. https://antorr.co/academia/mse-full-height-dynamo-revit.html [artículo citado]
- Berg, R. R., Christopher, B. R., & Samtani, N. C. (2009). Mechanically stabilized earth walls and reinforced soil slopes design & construction guidelines (FHWA-NHI-10-024). Federal Highway Administration. https://www.fhwa.dot.gov/engineering/geotech/pubs/nhi10024/
- ISO 19650-1:2018. Organization and digitization of information about buildings and civil engineering works, including building information modelling (BIM) — Information management using BIM. Part 1: Concepts and principles. International Organization for Standardization.
- ISO 19650-2:2018. Information management using BIM. Part 2: Delivery phase of the assets. International Organization for Standardization.
- AASHTO. (2007). LRFD bridge design specifications (4th ed.). American Association of State Highway and Transportation Officials.
- Terzidis, K. (2006). Algorithmic architecture. Elsevier.
- Autodesk Inc. (2025). Dynamo Primer — A guide to visual programming for design. Autodesk. https://primer.dynamobim.org
- Autodesk Inc. (2025). Revit API Developer's Guide 2026. Autodesk Developer Network. https://www.revitapidocs.com
- Eastman, C., Teicholz, P., Sacks, R., & Liston, K. (2011). BIM Handbook: A guide to building information modeling for owners, managers, designers, engineers, and contractors (2nd ed.). John Wiley & Sons.