Calcular cubicaciones en Revit siempre fue un proceso que me molestó. Exportabas a Excel, filtrabas a mano, y si el modelo cambiaba — cosa que siempre pasa — volvías a empezar. En este artículo te muestro cómo construí un script que resuelve eso en 10 segundos.
2 h
Proceso manual
10 s
Con el plugin
720×
Más rápido
El flujo clásico de cubicación en cualquier oficina de proyectos se ve más o menos así: abres una vista en Revit, exportas una planilla de cantidades, la abres en Excel, filtras por tipo de material, calculas los pesos con fórmulas que alguien hizo hace tres años y nadie sabe si son correctas, y al final generas un reporte.
El problema no es que sea lento — aunque lo es. El problema es que el modelo y la planilla se desconectan desde el momento en que exportas. Cualquier cambio en el modelo requiere repetir todo el proceso desde cero.
En un proyecto de mediana envergadura, un ciclo completo de cubicación manual puede tomar entre 2 y 4 horas. Si el modelo cambia dos veces a la semana — lo cual es normal — estás gastando casi un día completo en extraer datos que ya existen en el modelo. La solución no es trabajar más rápido. Es no hacer ese trabajo en absoluto.
El script extrae directamente desde el modelo Revit, a través de su API, toda la información necesaria para generar una cubicación completa de acero y hormigón. Sin exportar nada, sin abrir Excel, sin fórmulas manuales.
No necesitas saber Python para usar este script. El tutorial te explica dónde poner cada archivo. Si quieres entender cómo funciona el código, las secciones de explicación son para ti.
Script listo para producción. Cópialo directamente en tu script.py:
# -*- coding: utf-8 -*-
# Cubicación Automática — Andrés Gallo P.
# atelijudesign.com
import clr
clr.AddReference('RevitAPI')
clr.AddReference('RevitAPIUI')
from Autodesk.Revit.DB import (
FilteredElementCollector,
BuiltInCategory,
BuiltInParameter,
UnitUtils,
UnitTypeId
)
from pyrevit import revit, DB, forms, script
# ─── CONFIGURACIÓN ──────────────────────────────────────────────
DENSIDAD_ACERO = 7850 # kg/m³
DENSIDAD_HORMIGON = 2400 # kg/m³
FACTOR_CONEXIONES = 0.05 # 5% por defecto — ajústalo según proyecto
# Clasificación Nominal Weight (kg/m)
CLASES_NW = [
(0, 20, "Liviana"),
(20, 40, "Media"),
(40, 80, "Pesada"),
(80, 9999, "Extrapesada"),
]
# ─── FUNCIONES AUXILIARES ────────────────────────────────────────
def m3_a_kg(volumen_m3, densidad):
"""Convierte volumen en m³ a kilogramos."""
return volumen_m3 * densidad
def clasificar_nw(peso_lineal_kg_m):
"""Clasifica un perfil por Nominal Weight según su peso lineal."""
for min_nw, max_nw, nombre in CLASES_NW:
if min_nw <= peso_lineal_kg_m < max_nw:
return nombre
return "Sin clasificar"
def obtener_volumen_m3(elemento):
"""Extrae el volumen del elemento en m³ desde la API de Revit."""
try:
param = elemento.get_Parameter(BuiltInParameter.HOST_VOLUME_COMPUTED)
if param and param.HasValue:
# Revit almacena en pies³ — convertimos a m³
return UnitUtils.ConvertFromInternalUnits(
param.AsDouble(),
UnitTypeId.CubicMeters
)
except:
pass
return 0.0
def obtener_nombre_material(elemento):
"""Obtiene el nombre del material estructural del elemento."""
try:
param = elemento.get_Parameter(BuiltInParameter.STRUCTURAL_MATERIAL_PARAM)
if param and param.HasValue:
mat_id = param.AsElementId()
material = elemento.Document.GetElement(mat_id)
if material:
return material.Name.upper()
except:
pass
return ""
def es_acero(nombre_material):
"""Determina si el material es acero por palabras clave."""
keywords = ["ACERO", "STEEL", "A36", "A572", "A992", "ER70", "METAL"]
return any(k in nombre_material for k in keywords)
def es_hormigon(nombre_material):
"""Determina si el material es hormigón por palabras clave."""
keywords = ["HORMIGON", "HORMIGÓN", "CONCRETE", "HA", "H20", "H25", "H30"]
return any(k in nombre_material for k in keywords)
# ─── RECOLECCIÓN DE ELEMENTOS ────────────────────────────────────
doc = revit.doc
categorias = [
BuiltInCategory.OST_StructuralFraming, # vigas, diagonales
BuiltInCategory.OST_StructuralColumns, # columnas
BuiltInCategory.OST_StructuralFoundation # fundaciones
]
elementos = []
for cat in categorias:
collector = FilteredElementCollector(doc)\
.OfCategory(cat)\
.WhereElementIsNotElementType()
elementos.extend(list(collector))
# ─── PROCESAMIENTO ───────────────────────────────────────────────
resultado_acero = {} # {clase_nw: [pesos]}
total_hormigon_m3 = 0.0
elementos_sin_mat = 0
for elem in elementos:
volumen = obtener_volumen_m3(elem)
if volumen <= 0:
continue
nombre_mat = obtener_nombre_material(elem)
if es_acero(nombre_mat):
# Peso lineal para clasificación NW
try:
param_largo = elem.get_Parameter(BuiltInParameter.STRUCTURAL_FRAME_CUT_LENGTH)
largo_m = UnitUtils.ConvertFromInternalUnits(
param_largo.AsDouble(), UnitTypeId.Meters
) if param_largo and param_largo.HasValue else 1.0
except:
largo_m = 1.0
peso_total_kg = m3_a_kg(volumen, DENSIDAD_ACERO)
peso_lineal = peso_total_kg / largo_m if largo_m > 0 else 0
clase_nw = clasificar_nw(peso_lineal)
if clase_nw not in resultado_acero:
resultado_acero[clase_nw] = []
resultado_acero[clase_nw].append(peso_total_kg)
elif es_hormigon(nombre_mat):
total_hormigon_m3 += volumen
else:
elementos_sin_mat += 1
# ─── CÁLCULO DE TOTALES ──────────────────────────────────────────
total_acero_kg = sum(sum(p) for p in resultado_acero.values())
total_con_conx = total_acero_kg * (1 + FACTOR_CONEXIONES)
total_hormigon_kg = m3_a_kg(total_hormigon_m3, DENSIDAD_HORMIGON)
# ─── GENERACIÓN DEL REPORTE ──────────────────────────────────────
output = script.get_output()
output.close_others()
output.set_title("Cubicación Estructural — {}".format(doc.Title))
output.print_md("# 📊 Reporte de Cubicación Estructural")
output.print_md("**Proyecto:** {}".format(doc.Title))
output.print_md("**Elementos procesados:** {}".format(len(elementos)))
if elementos_sin_mat > 0:
output.print_md("⚠️ **Elementos sin material asignado:** {}".format(elementos_sin_mat))
# Tabla acero por clase NW
output.print_md("---")
output.print_md("## 🔩 Acero Estructural")
tabla_acero = [["Clase NW", "N° Elementos", "Peso (kg)", "Peso (ton)"]]
for clase in ["Liviana", "Media", "Pesada", "Extrapesada"]:
if clase in resultado_acero:
pesos = resultado_acero[clase]
total = sum(pesos)
tabla_acero.append([
clase,
str(len(pesos)),
"{:,.1f}".format(total),
"{:,.2f}".format(total / 1000)
])
output.print_table(
table_data=tabla_acero[1:],
title="Clasificación por Nominal Weight",
columns=tabla_acero[0]
)
output.print_md("**Subtotal acero:** {:,.1f} kg ({:,.2f} ton)".format(
total_acero_kg, total_acero_kg / 1000))
output.print_md("**Factor conexiones ({:.0f}%):** + {:,.1f} kg".format(
FACTOR_CONEXIONES * 100, total_acero_kg * FACTOR_CONEXIONES))
output.print_md("### ✅ Total acero c/conexiones: {:,.1f} kg ({:,.2f} ton)".format(
total_con_conx, total_con_conx / 1000))
# Tabla hormigón
output.print_md("---")
output.print_md("## 🏗️ Hormigón Estructural")
output.print_md("**Volumen total:** {:,.2f} m³".format(total_hormigon_m3))
output.print_md("**Peso total:** {:,.1f} kg ({:,.2f} ton)".format(
total_hormigon_kg, total_hormigon_kg / 1000))
output.print_md("---")
output.print_md("*Generado con pyRevit · atelijudesign.com*")
# ─── CONFIGURACIÓN ──────────────────────────────
DENSIDAD_ACERO = 7850 # kg/m³ — no cambiar
DENSIDAD_HORMIGON = 2400 # kg/m³ — ajusta si tu especificación difiere
FACTOR_CONEXIONES = 0.05 # 5% estándar
# 0.08 = 8% para proyectos industriales
# 0.12 = 12% para estructuras complejas
Si el modelo tiene materiales correctamente asignados, el reporte se ve así en la consola de pyRevit:
📊 Reporte de Cubicación Estructural Proyecto: Proyecto_Arqueros_Rev3 Elementos procesados: 847 ───────────────────────────────────────────── 🔩 Acero Estructural — Clasificación por NW ───────────────────────────────────────────── Clase NW N° Elementos Peso (kg) Peso (ton) Liviana 124 8.420,3 8,42 Media 298 42.180,7 42,18 Pesada 201 89.340,2 89,34 Extrapesada 48 38.920,1 38,92 Subtotal acero: 178.861,3 kg (178,86 ton) Factor conexiones (5%): + 8.943,1 kg ✅ Total c/conexiones: 187.804,4 kg (187,80 ton) ───────────────────────────────────────────── 🏗️ Hormigón Estructural ───────────────────────────────────────────── Volumen total: 1.240,50 m³ Peso total: 2.977.200 kg (2.977,20 ton)
La función obtener_nombre_material() lee el parámetro STRUCTURAL_MATERIAL_PARAM de cada elemento, obtiene el ID del material asignado y consulta su nombre en el documento. Las funciones es_acero() y es_hormigon() buscan palabras clave en ese nombre — por eso es importante que tus materiales en Revit tengan nombres descriptivos.
Revit calcula el volumen real de cada elemento estructural y lo almacena en el parámetro HOST_VOLUME_COMPUTED. La API lo entrega en pies cúbicos — la conversión a m³ la hace UnitUtils.ConvertFromInternalUnits(). Este valor ya considera la geometría exacta del perfil, no una aproximación.
El volumen calculado por Revit considera los cortes, uniones y modificaciones de geometría que aplicaste al modelo. Una tabla de Excel con fórmulas de largo × área de sección no hace eso. La diferencia puede ser de hasta 3–5% en estructuras con muchas uniones.
El script calcula el peso lineal de cada elemento dividiendo el peso total por el largo de corte estructural. Con ese valor clasifica el perfil en una de las cuatro categorías estándar. Esto es útil para separar las cotizaciones por tipo de perfil, que en Chile generalmente tienen precios distintos por categoría NW.
Tiempo invertido por tarea en el flujo manual versus el flujo automatizado:
Verifica que el nombre de tu carpeta termine exactamente en .extension, .tab, .panel y .pushbutton. Un espacio de más o una mayúscula incorrecta hace que pyRevit no lo reconozca. Luego haz Reload.
Si el reporte muestra elementos sin material, abre el modelo, selecciona esos elementos y asigna el material estructural correcto en las propiedades. El script avisa cuántos tienen ese problema.
En versiones anteriores a 2022, UnitTypeId no existe. Reemplaza la conversión por DisplayUnitType.DUT_CUBIC_METERS, que es el equivalente en versiones más antiguas.