Update:
I rewrite the entire code to make it robust. The main idea is to have a config button that allows the user to select a desired 3D view to work on it.
Once the user select his working view you can run the “Magic 3D view” tool to adjust the section box on the 3d view.
========================================================================
Config Script
# -*- coding: utf-8 -*-
"""
Configuration Button for 3D View Selection
This script allows users to select a 3D view and store it for future use.
"""
__title__ = "Config 3D View"
__author__ = "Your Name"
__doc__ = """Select and store a 3D view configuration for the model."""
# pyRevit imports
from pyrevit import revit, DB, UI
from pyrevit import script
from pyrevit import forms
from pyrevit.userconfig import user_config
# Standard library imports
import json
import os
# Get current document and logger
doc = revit.doc
logger = script.get_logger()
# Configuration file identifier
CONFIG_FILE_ID = 'selected_3d_view_config'
def get_all_3d_views():
"""
Collect all 3D views from the current model.
Returns:
list: List of 3D View elements
"""
try:
# Create a filtered element collector for 3D views
collector = DB.FilteredElementCollector(doc)
# Filter for ViewType.ThreeD
three_d_views = collector.OfClass(DB.View3D).ToElements()
# Filter out templates and non-accessible views
valid_views = []
for view in three_d_views:
if not view.IsTemplate and view.CanBePrinted:
valid_views.append(view)
logger.debug("Found {} valid 3D views".format(len(valid_views)))
return valid_views
except Exception as e:
logger.error("Error collecting 3D views: {}".format(str(e)))
return []
def create_view_selection_list(views):
"""
Create a list of view names with their IDs for selection.
Args:
views (list): List of 3D View elements
Returns:
list: List of dictionaries with view info
"""
view_options = []
for view in views:
view_info = {
'name': view.Name,
'id': view.Id.IntegerValue,
'element': view
}
view_options.append(view_info)
return view_options
def load_existing_config():
"""
Load existing configuration data if it exists.
Returns:
dict or None: Configuration data or None if not found
"""
try:
# Get the data file path
data_file = script.get_instance_data_file(CONFIG_FILE_ID, add_cmd_name=False)
if os.path.exists(data_file):
with open(data_file, 'r') as f:
config_data = json.load(f)
logger.debug("Loaded existing configuration: {}".format(config_data))
return config_data
else:
logger.debug("No existing configuration found")
return None
except Exception as e:
logger.error("Error loading configuration: {}".format(str(e)))
return None
def store_data(selected_view):
"""
Store the selected view data to configuration file.
Args:
selected_view (dict): Dictionary containing view information
Returns:
bool: True if successful, False otherwise
"""
try:
# Prepare data to store
config_data = {
'selected_view_id': selected_view['id'],
'selected_view_name': selected_view['name'],
'model_path': doc.PathName
}
# Get the data file path
data_file = script.get_instance_data_file(CONFIG_FILE_ID, add_cmd_name=False)
# Ensure directory exists
data_dir = os.path.dirname(data_file)
if not os.path.exists(data_dir):
os.makedirs(data_dir)
# Write configuration data
with open(data_file, 'w') as f:
json.dump(config_data, f, indent=2)
logger.debug("Configuration stored successfully: {}".format(data_file))
return True
except Exception as e:
logger.error("Error storing configuration: {}".format(str(e)))
return False
def main():
"""
Main execution function for the configuration button.
"""
try:
# Step 1: Check for existing configuration
existing_config = load_existing_config()
if existing_config:
logger.info("Found existing configuration for view: {}".format(
existing_config.get('selected_view_name', 'Unknown')))
# Step 2: Collect all 3D views from the model
all_3d_views = get_all_3d_views()
if not all_3d_views:
forms.alert("No 3D views found in the current model.",
title="Configuration Error",
exitscript=True)
# Step 3: Create selection options
view_options = create_view_selection_list(all_3d_views)
view_names = [view['name'] for view in view_options]
# Step 4: Ask user for selection using SelectFromList
selected_view_name = forms.SelectFromList.show(
view_names,
title="Select 3D View for Configuration",
message="Choose the 3D view to store in configuration:",
multiselect=False
)
if not selected_view_name:
# User cancelled selection
logger.info("User cancelled view selection")
return
# Step 5: Find the selected view data
selected_view = None
for view_option in view_options:
if view_option['name'] == selected_view_name:
selected_view = view_option
break
if not selected_view:
forms.alert("Error: Could not find selected view data.",
title="Configuration Error")
return
# Step 6: Store the selected view data
storage_success = store_data(selected_view)
# Step 7: Show confirmation to user
if storage_success:
forms.alert(
"Configuration completed successfully!\n\n"
"Selected 3D View: {}\n"
"View ID: {}".format(
selected_view['name'],
selected_view['id']
),
title="Configuration Completed",
sub_msg="The 3D view selection has been saved.",
warn_icon=False
)
else:
forms.alert(
"Error: Configuration could not be completed.\n"
"Please check the console for more details.",
title="Configuration Error"
)
except Exception as e:
logger.error("Main execution error: {}".format(str(e)))
forms.alert(
"An unexpected error occurred during configuration.\n"
"Error: {}".format(str(e)),
title="Configuration Error"
)
# Execute main function
if __name__ == '__main__':
main()
3D Magic View
# -*- coding: utf-8 -*-
__title__ = "Magic 3D View"
__author__ = "Antonio Rojas"
__doc__ = """Modify section box of the configured 3D view based on
a rectangular selection. Works on floor plans, ceiling and
section views.
Updated version that uses the 3D view selected in configuration.
Original idea: Pangolin Tools
Original version by: Stéphane ROSPARS DUPIN
Modified by: Ing Arq Antonio Rojas
Updated for configuration integration
"""
# Importaciones necesarias
from Autodesk.Revit.DB import *
from Autodesk.Revit.UI import *
from pyrevit import revit, forms, HOST_APP, script
import json
import os
# Configuration file identifier (same as config script)
CONFIG_FILE_ID = 'selected_3d_view_config'
# Cache para almacenar vistas encontradas y evitar búsquedas repetidas
_vista_cache = {}
def cargar_configuracion_vista():
"""
Carga la configuración de la vista 3D seleccionada previamente.
Returns:
dict or None: Datos de configuración o None si no existe
"""
try:
# Obtener el archivo de datos usando el mismo método que el script de configuración
data_file = script.get_instance_data_file(CONFIG_FILE_ID, add_cmd_name=False)
if os.path.exists(data_file):
with open(data_file, 'r') as f:
config_data = json.load(f)
# Validar que la configuración tenga los datos necesarios
if 'selected_view_id' in config_data:
# print("Configuración cargada - Vista: {}".format(
# config_data.get('selected_view_name', 'Desconocida')))
return config_data
else:
# print("Configuración inválida - falta ID de vista")
return None
else:
print("No se encontró archivo de configuración")
return None
except Exception as ex:
print("Error al cargar configuración: {}".format(str(ex)))
return None
def obtener_vista_3d_configurada():
"""
Obtiene la vista 3D configurada desde el archivo de configuración.
Returns:
View3D: Vista 3D configurada o None si no se encuentra
"""
try:
# Cargar configuración
config_data = cargar_configuracion_vista()
if not config_data:
return None
# Obtener ID de la vista configurada
view_id = config_data.get('selected_view_id')
if not view_id:
print("Error: No se encontró ID de vista en configuración")
return None
# Buscar la vista por ID
element_id = ElementId(view_id)
vista_elemento = revit.doc.GetElement(element_id)
if vista_elemento and isinstance(vista_elemento, View3D):
# print("Vista 3D configurada encontrada: {}".format(vista_elemento.Name))
return vista_elemento
else:
print("Error: La vista configurada no existe o no es válida")
return None
except Exception as ex:
print("Error al obtener vista 3D configurada: {}".format(str(ex)))
return None
def es_vista_soportada(vista):
"""
Verifica si el tipo de vista es compatible con el script.
Args:
vista: Objeto View de Revit
Returns:
bool: True si la vista es soportada (planta, ingeniería, techo, corte)
"""
tipos_soportados = {
ViewType.FloorPlan,
ViewType.EngineeringPlan,
ViewType.CeilingPlan,
ViewType.Section
}
return vista.ViewType in tipos_soportados
def es_vista_planta(vista):
"""
Verifica si la vista es de tipo planta (no corte).
Args:
vista: Objeto View de Revit
Returns:
bool: True si es vista de planta
"""
tipos_planta = {
ViewType.FloorPlan,
ViewType.EngineeringPlan,
ViewType.CeilingPlan
}
return vista.ViewType in tipos_planta
def hacer_zoom_a_bbox(vista, bbox):
"""
Realiza zoom a los elementos contenidos dentro de una BoundingBox.
OPTIMIZACIÓN: Usa filtro más eficiente y manejo de errores.
Args:
vista: Vista donde hacer el zoom
bbox: BoundingBox objetivo
"""
try:
# Crear filtro de intersección optimizado
outline = Outline(bbox.Min, bbox.Max)
filtro_bbox = BoundingBoxIntersectsFilter(outline)
# Usar collector específico para la vista (más rápido que doc completo)
ids_en_caja = (FilteredElementCollector(revit.doc, vista.Id)
.WherePasses(filtro_bbox)
.ToElementIds())
# Solo mostrar si hay elementos
if ids_en_caja and ids_en_caja.Count > 0:
revit.uidoc.ShowElements(ids_en_caja)
except Exception as ex:
print("Error en zoom_to_bbox: {}".format(str(ex)))
def obtener_crop_box_desde_rango_vista(vista):
"""
Construye una BoundingBox desde el rango de vista de una planta.
OPTIMIZACIÓN: Simplificado y con mejor manejo de errores.
Args:
vista: Vista de planta activa
Returns:
BoundingBoxXYZ: Caja delimitadora configurada
"""
try:
# Selección de área por el usuario
estilo_seleccion = Selection.PickBoxStyle.Crossing
caja_seleccionada = revit.uidoc.Selection.PickBox(
estilo_seleccion,
"Arrastra una caja de selección sobre el área deseada"
)
# Coordenadas de la selección del usuario
punto_min = caja_seleccionada.Min
punto_max = caja_seleccionada.Max
# Obtener rango de vista (elevaciones)
rango_vista = vista.GetViewRange()
# IDs de niveles inferior y superior
id_nivel_inferior = rango_vista.GetLevelId(PlanViewPlane.BottomClipPlane)
id_nivel_superior = rango_vista.GetLevelId(PlanViewPlane.TopClipPlane)
# Obtener elevaciones con valores por defecto más seguros
elevacion_inferior = 0.0
elevacion_superior = 9.84 # 3 metros por defecto
# Procesar nivel inferior
if id_nivel_inferior != ElementId.InvalidElementId:
nivel_inferior = revit.doc.GetElement(id_nivel_inferior)
if nivel_inferior:
offset_inferior = rango_vista.GetOffset(PlanViewPlane.BottomClipPlane)
elevacion_inferior = nivel_inferior.ProjectElevation + offset_inferior
# Procesar nivel superior
if id_nivel_superior != ElementId.InvalidElementId:
nivel_superior = revit.doc.GetElement(id_nivel_superior)
if nivel_superior:
offset_superior = rango_vista.GetOffset(PlanViewPlane.TopClipPlane)
elevacion_superior = nivel_superior.ProjectElevation + offset_superior
# Crear BoundingBox optimizada
bbox = BoundingBoxXYZ()
# Asegurar que Min tenga valores menores y Max valores mayores
bbox.Min = XYZ(
min(punto_min.X, punto_max.X),
min(punto_min.Y, punto_max.Y),
elevacion_inferior
)
bbox.Max = XYZ(
max(punto_min.X, punto_max.X),
max(punto_min.Y, punto_max.Y),
elevacion_superior
)
return bbox
except Exception as ex:
print("Error al obtener crop box desde rango de vista: {}".format(str(ex)))
return None
def reorientar_bbox_a_mundo(bbox):
"""
Transforma una BoundingBox del sistema local al sistema mundial.
OPTIMIZACIÓN: Usa list comprehension más eficiente.
Args:
bbox: BoundingBox en coordenadas locales
Returns:
BoundingBoxXYZ: BoundingBox en coordenadas mundiales
"""
try:
if not bbox or not bbox.Transform:
return bbox
# Generar esquinas del cubo local de forma más eficiente
min_local = bbox.Min
max_local = bbox.Max
esquinas_locales = [
XYZ(x, y, z)
for x in [min_local.X, max_local.X]
for y in [min_local.Y, max_local.Y]
for z in [min_local.Z, max_local.Z]
]
# Transformar a coordenadas mundiales
transform = bbox.Transform
esquinas_mundiales = [transform.OfPoint(punto) for punto in esquinas_locales]
# Encontrar límites globales de forma optimizada
coords_x = [p.X for p in esquinas_mundiales]
coords_y = [p.Y for p in esquinas_mundiales]
coords_z = [p.Z for p in esquinas_mundiales]
# Crear nueva BoundingBox
nueva_bbox = BoundingBoxXYZ()
nueva_bbox.Min = XYZ(min(coords_x), min(coords_y), min(coords_z))
nueva_bbox.Max = XYZ(max(coords_x), max(coords_y), max(coords_z))
nueva_bbox.Transform = Transform.Identity
return nueva_bbox
except Exception as ex:
print("Error al reorientar bbox: {}".format(str(ex)))
return bbox
def aplicar_cropbox_a_vista3d_configurada(cropbox):
"""
Aplica la cropbox como section box a la vista 3D configurada.
ACTUALIZADO: Usa la vista configurada en lugar de buscar por nombre de usuario.
Args:
cropbox: BoundingBox a aplicar
Returns:
bool: True si se aplicó correctamente
"""
if not cropbox:
print("Error: cropbox es None")
return False
# Obtener vista 3D configurada
vista_objetivo = obtener_vista_3d_configurada()
if not vista_objetivo:
forms.alert(
"❌ Vista 3D no configurada ❌",
title="Error de configuración",
sub_msg="No se encontró una vista 3D configurada.\n\n"
"Ejecuta primero el botón de **Configuración** para "
"seleccionar una vista 3D.",
ok=True,
exitscript=True,
warn_icon=True
)
return False
# print("Aplicando cropbox a vista configurada: {}".format(vista_objetivo.Name))
# Aplicar cambios en transacción optimizada
try:
with Transaction(revit.doc, "Aplicar CropBox a Vista 3D Configurada") as transaccion:
transaccion.Start()
# Aplicar section box
vista_objetivo.SetSectionBox(cropbox)
# Remover template si existe (para evitar conflictos)
if vista_objetivo.ViewTemplateId != ElementId.InvalidElementId:
vista_objetivo.ViewTemplateId = ElementId.InvalidElementId
# Ocultar categorías no deseadas de forma batch
categorias_ocultar = [
ElementId(BuiltInCategory.OST_Levels), # Niveles
ElementId(BuiltInCategory.OST_VolumeOfInterest) # Zonas de interés
]
for categoria_id in categorias_ocultar:
try:
vista_objetivo.SetCategoryHidden(categoria_id, True)
except:
# Continuar si alguna categoría no se puede ocultar
pass
transaccion.Commit()
# Activar vista y refrescar
revit.uidoc.ActiveView = vista_objetivo
revit.uidoc.RefreshActiveView()
# print("✅ CropBox aplicado exitosamente a: {}".format(vista_objetivo.Name))
return True
except Exception as ex:
print("Error al aplicar cropbox: {}".format(str(ex)))
return False
def obtener_crop_box_transformado_corte(vista):
"""
Obtiene cropbox transformado para vistas de corte.
OPTIMIZACIÓN: Proceso simplificado y mejor manejo de errores.
Args:
vista: Vista de corte activa
Returns:
BoundingBoxXYZ: Caja transformada
"""
try:
# Selección del usuario
estilo_seleccion = Selection.PickBoxStyle.Crossing
caja_seleccionada = revit.uidoc.Selection.PickBox(
estilo_seleccion,
"Selecciona el área en la vista de corte"
)
punto_min_usuario = caja_seleccionada.Min
punto_max_usuario = caja_seleccionada.Max
# Obtener cropbox actual de la vista
bbox_vista = vista.CropBox
if not bbox_vista or not bbox_vista.Enabled:
print("Warning: CropBox no disponible, usando selección del usuario")
# Crear bbox básico desde selección
bbox_transformado = BoundingBoxXYZ()
bbox_transformado.Min = punto_min_usuario
bbox_transformado.Max = punto_max_usuario
return bbox_transformado
# Aplicar transformación al sistema global
transform = bbox_vista.Transform
punto_min_vista = transform.OfPoint(bbox_vista.Min)
punto_max_vista = transform.OfPoint(bbox_vista.Max)
# Combinar coordenadas de vista y selección del usuario
bbox_transformado = BoundingBoxXYZ()
bbox_transformado.Min = XYZ(
min(punto_min_vista.X, punto_max_vista.X),
min(punto_min_usuario.Y, punto_max_usuario.Y),
min(punto_min_vista.Z, punto_max_vista.Z)
)
bbox_transformado.Max = XYZ(
max(punto_min_vista.X, punto_max_vista.X),
max(punto_min_usuario.Y, punto_max_usuario.Y),
max(punto_min_vista.Z, punto_max_vista.Z)
)
return bbox_transformado
except Exception as ex:
print("Error al obtener crop box transformado: {}".format(str(ex)))
return None
def mostrar_info_configuracion():
"""
Muestra información sobre la configuración actual antes de ejecutar.
"""
config_data = cargar_configuracion_vista()
if config_data:
vista_nombre = config_data.get('selected_view_name', 'Desconocida')
# print("📋 Configuración actual:")
# print(" Vista 3D: {}".format(vista_nombre))
# print(" Configurada: {}".format(config_data.get('timestamp', 'N/A')))
else:
print("⚠️ No hay configuración disponible")
def ejecutar_script_principal():
"""
Función principal optimizada del script.
ACTUALIZADO: Verifica configuración antes de proceder.
"""
try:
# Mostrar información de configuración
mostrar_info_configuracion()
# Verificar que existe configuración
if not cargar_configuracion_vista():
forms.alert(
"❌ No hay configuración ❌",
title="Configuración requerida",
sub_msg="Antes de usar Magic 3D View, debes:\n\n"
"1. Ejecutar el botón de **Configuración**\n"
"2. Seleccionar una vista 3D\n"
"3. Luego ejecutar este comando",
ok=True,
exitscript=True,
warn_icon=True
)
return
# Obtener vista activa
vista_activa = revit.active_view
# Validación temprana de tipo de vista
if not es_vista_soportada(vista_activa):
forms.alert(
"Tipo de vista no soportado",
title="Error de formato de vista",
sub_msg="Ejecuta el comando desde:\n• Plan de piso\n• Plan de techo\n• Vista de ingeniería\n• Vista de corte",
ok=True,
exitscript=True,
warn_icon=True
)
return
# print("Procesando vista: {} - Tipo: {}".format(
# vista_activa.Name,
# vista_activa.ViewType
# ))
# Procesar según tipo de vista
bbox_resultado = None
if es_vista_planta(vista_activa):
# print("Procesando como vista de planta...")
bbox_resultado = obtener_crop_box_desde_rango_vista(vista_activa)
else:
# print("Procesando como vista de corte...")
bbox_resultado = obtener_crop_box_transformado_corte(vista_activa)
# Validar resultado
if not bbox_resultado:
forms.alert(
"Error al procesar la selección",
title="Error de procesamiento",
sub_msg="No se pudo crear la caja delimitadora desde la selección",
ok=True,
warn_icon=True
)
return
# Aplicar a vista 3D configurada
# print("Aplicando cropbox a vista 3D configurada...")
exito = aplicar_cropbox_a_vista3d_configurada(bbox_resultado)
if exito:
# print("✅ Script ejecutado exitosamente")
# Mostrar confirmación adicional
config_data = cargar_configuracion_vista()
vista_nombre = config_data.get('selected_view_name', 'Configurada')
# forms.alert(
# "✅ Magic 3D View aplicado ✅",
# title="Operación completada",
# sub_msg="La vista 3D **{}** ha sido actualizada "
# "con la selección realizada.".format(vista_nombre),
# ok=True
# )
else:
print("❌ Error al aplicar cropbox")
except Exception as ex:
print("Error crítico en script principal: {}".format(str(ex)))
forms.alert(
"Error crítico",
title="Error de ejecución",
sub_msg="Se produjo un error inesperado:\n{}".format(str(ex)),
ok=True,
warn_icon=True
)
def limpiar_cache():
"""Limpia el cache de vistas."""
global _vista_cache
_vista_cache.clear()
# ================================================================
# 🚀 PUNTO DE ENTRADA PRINCIPAL
# ================================================================
if __name__ == "__main__":
try:
ejecutar_script_principal()
finally:
# Limpiar cache independientemente del resultado
limpiar_cache()
I would like to share this with everyone, might be useful for someone.
Coder cheers.