Creating a new 3D view with section box aligned to 2D crob box and depth

Hi everyone,

I am building my first script for work and decided to build one that takes you straight to a 3D view of any 2D view that you are in at that moment.

Everything is working perfectly, except for the cropping of the section box on to the 2 view crop region.

I am using the SetSectionBox(my3Dview, boundingBoxXYZ) method, my problem is that I can’t seem to find anything that returns a boundingBoxXYZ that is usable in my script.

Please see my code below for reference:

"""Create 3D view from current view"""

from pyrevit import revit, DB, UI
from pyrevit import forms
from pyrevit import script

#Create new 3D view with name of current user, cropped to the active view
def new_view(view, user):
    if type(view) == revit.DB.View3D:
        forms.alert("Please use a 2D view")
    else:
        with script.revit.Transaction("Create View"):
            newThreeDee = revit.create.create_3d_view(user + " temp")
        revit.UI.UIDocument.RequestViewChange(revit.HOST_APP.uidoc, newThreeDee)

        revit.DB.View3D.SetSectionBox(newThreeDee, revit.DB.View.getBoundingBox(view, view))
        # print(type(revit.DB.View.GetCropRegionShapeManagerForReferenceCallout(revit.HOST_APP.doc, revit.DB.View.CropBox(newThreeDee))))

originalView = revit.active_view

#Check if active view is a 3D view
#Return false if view is 2D
get_view_type = type(originalView) == revit.DB.View3D

#Gets the name of the user currently active
get_current_user = revit.HOST_APP.username
             
new_view(originalView, get_current_user)```

Here’s a section from a similar script I’ve written.

elevation = view.GenLevel.LookupParameter('Elevation').AsDouble()
vCrop = list(view.GetCropRegionShapeManager().GetCropShape())[0]
vCropPointsX = []
vCropPointsY = []
for x in vCrop:
    vCropPointsX.append(x.GetEndPoint(0).X)
    vCropPointsY.append(x.GetEndPoint(0).Y)
    vCropPointsX.append(x.GetEndPoint(1).X)
    vCropPointsY.append(x.GetEndPoint(1).Y)
minX = min(vCropPointsX)
minY = min(vCropPointsY)
maxX = max(vCropPointsX)
maxY = max(vCropPointsY)           

new_box = BoundingBoxXYZ()
new_box.Min = XYZ(minX, minY, elevation - 10)
new_box.Max = XYZ(maxX, maxY, elevation + 10)
v_name = view.Name
new_iso = DB.View3D.CreateIsometric(doc,threeDTypes[0].Id)
new_iso.SetSectionBox(new_box)
name_param = new_iso.LookupParameter('View Name')
name_param.Set('{} ISOMETRIC'.format(v_name))

This creates a 3D view cropped to the maximum extents of the view. The section box is always orthogonal to project north in the above code, if you want to look into rotated section boxes, you can dive into BoundingBoxXYZ.Transform.
ApiDocs.co · Revit · Transform Property

2 Likes

Yes, you have to create a new section box based on the view range of the view. In the example it has a fixed height, but it is better to use the actual view range.

1 Like

Thank you very much! I will give it a shot today!

Hi, i have the same idea for a while in my head, did you manage to solve your problem? Got the same problem with the BoundingBox.

1 Like

I use UI.Selection.PickBox to have users draw a box around where they want the 3D view and create a bounding box with the 2 points returned using the bounding box constructor. Please note that if the viewplan has a tranform applied to it you will need to apply the views transform to the points to rotate and/or translate the coordinates to the correct location. Otherwise the 3D section box will not closely match what you drew. Here’s my code to get the the box and then my fuction to create the DB.BoundingBox:
from Autodesk.Revit.UI.Selection import PickBoxStyle
opt = PickBoxStyle.Crossing
try:
picked_box = HOST_APP.uidoc.Selection.PickBox(
opt, “Draw Rectangle around area to create 3D view”)
except Exception as e:
forms.alert(str(e), exitscript=True)

def create_xzybox(boundmin,boundmax, top, bottom):

min_pbox = DB.XYZ(boundmin[0], boundmin[1], bottom)
max_pbox = DB.XYZ(boundmax[0], boundmax[1], (top + 2.3))   

box = DB.BoundingBoxXYZ()
box.Min = min_pbox
box.Max = max_pbox
return box

For those still looking for it, python code for 3d view from active view, also works for sections.

#### coding: utf8

__title__   = "STR_3D depuis la vue"
__author__  = "Stéphane ROSPARS DUPIN"
__version__ = "Version: 1.0"
__doc__ = """Version = 1.0
Date    = 2024.03.26
_____________________________________________________________________
Description:
Passe la vue 3D utilisateur avec une zone de coupe alignée sur la vue active.
_____________________________________________________________________
Mode d'emploi:

_____________________________________________________________________
Last update:
- [024.03.26] - 1.0 Création
_____________________________________________________________________
Contact et support:
- Stéphane ROSPARS DUPIN - AIA ANGERS
_____________________________________________________________________
To-Do:
-> Ø
_____________________________________________________________________"""
#####################################################################
#📚📚📚📚---------------------------------------------BIBLIOTHEQUES:
#####################################################################

from Autodesk.Revit.DB import *
from pyrevit import revit, HOST_APP, script, forms
from Autodesk.Revit.DB import BoundingBoxIntersectsFilter, Outline, FilteredElementCollector


#####################################################################
#*️⃣0️⃣1️⃣2️⃣------------------------------------------------VARIABLES:
#####################################################################
uidoc           = __revit__.ActiveUIDocument
doc             = __revit__.ActiveUIDocument.Document #type:Document
app             = __revit__.Application
rvt_year        = int(app.VersionNumber)
curview         = revit.active_view
selection       = revit.get_selection()
#📺----------------------------------------------------------Sorties:
output 			= script.get_output()
output.close_others()          #Ferme la fenêtre de script précédente


#####################################################################
#🧬🧬🧬🧬------------------------------------------------FONCTIONS:
#####################################################################
def is_supported_view(view):
    return view.ViewType in [ViewType.FloorPlan, ViewType.EngineeringPlan,ViewType.CeilingPlan, ViewType.Section]
def is_viewplan(view):
    return view.ViewType in [ViewType.FloorPlan, ViewType.EngineeringPlan,ViewType.CeilingPlan]


def zoom_to_bbox(view, bbox):
    """Fait un zoom étendu sur une BoundingBox donnée dans une vue donnée."""
    outline = Outline(bbox.Min, bbox.Max)
    filter_bbox = BoundingBoxIntersectsFilter(outline)
    
    ids_in_box = FilteredElementCollector(doc, view.Id).WherePasses(filter_bbox).ToElementIds()
    
    if ids_in_box:
        revit.uidoc.ShowElements(ids_in_box)

def get_crop_box_from_view_range(view):
    """Construit une bounding box orientée à partir de la plage de vue (plans) ou fallback sur la bounding box brute."""
    
    # Récupération de la bounding box actuelle
    bbox_view = view.CropBox if view.CropBoxActive else view.get_BoundingBox(None)
    if not bbox_view:
        return None

    transform = bbox_view.Transform  # Important pour l'orientation !

    # X/Y depuis la bbox (fallback plus stable que CurveLoop)
    minX = bbox_view.Min.X
    maxX = bbox_view.Max.X
    minY = bbox_view.Min.Y
    maxY = bbox_view.Max.Y

    # Lecture de la plage de vue
    view_range = view.GetViewRange()
    bottom_id = view_range.GetLevelId(PlanViewPlane.BottomClipPlane)
    top_id = view_range.GetLevelId(PlanViewPlane.TopClipPlane)

    bottom_level = revit.doc.GetElement(bottom_id) if bottom_id != ElementId.InvalidElementId else None
    top_level = revit.doc.GetElement(top_id) if top_id != ElementId.InvalidElementId else None

    bottom_offset = view_range.GetOffset(PlanViewPlane.BottomClipPlane)
    top_offset = view_range.GetOffset(PlanViewPlane.TopClipPlane)

    bottom_elev = bottom_level.ProjectElevation + bottom_offset if bottom_level else bbox_view.Min.Z
    top_elev = top_level.ProjectElevation + top_offset if top_level else bbox_view.Max.Z

    # Création de la nouvelle bounding box
    bbox = BoundingBoxXYZ()
    bbox.Min = XYZ(minX, minY, bottom_elev)
    bbox.Max = XYZ(maxX, maxY, top_elev)
    bbox.Transform = transform  

    return bbox

# ------------------------------------------
# EXPAND / REORIENT BOUNDINGBOX
# ------------------------------------------
def reorient_bbox_to_world(bbox):
    """Transforme une BoundingBox orientée dans le repère local vers le repère monde."""
    local_min = bbox.Min
    local_max = bbox.Max
    corners_local = [XYZ(x, y, z) for x in [local_min.X, local_max.X]
                                  for y in [local_min.Y, local_max.Y]
                                  for z in [local_min.Z, local_max.Z]]
    corners_world = [bbox.Transform.OfPoint(p) for p in corners_local]
    min_x = min(p.X for p in corners_world)
    min_y = min(p.Y for p in corners_world)
    min_z = min(p.Z for p in corners_world)
    max_x = max(p.X for p in corners_world)
    max_y = max(p.Y for p in corners_world)
    max_z = max(p.Z for p in corners_world)
    new_bbox = BoundingBoxXYZ()
    new_bbox.Min = XYZ(min_x, min_y, min_z)
    new_bbox.Max = XYZ(max_x, max_y, max_z)
    new_bbox.Transform = Transform.Identity
    return new_bbox


def apply_cropbox_to_user3d(cropbox):
    user = str(HOST_APP.username)
    user_view_name = "{3D - " + user[0] + "." + user[1:] + "}"
    collector = FilteredElementCollector(revit.doc).OfCategory(BuiltInCategory.OST_Views)

    target_view = next((v for v in collector if v.Name == user_view_name and isinstance(v, View3D)), None)
    if not target_view: 
        user_view_name="{3D}"
        target_view=next((v for v in collector if v.Name == user_view_name and isinstance(v, View3D)), None)
    elif not target_view:
        #🚩🚧🚩-Alerte:
        forms.alert("❌ Vue 3D non trouvée❌",
                    title="Erreur vue utilisateur", 
                    sub_msg="**{}** non trouvée".format(user_view_name),
                    ok=True,
                    yes=False,
                    no=False,
                    exitscript=True,
                    warn_icon=False)
        return

    with Transaction(revit.doc, "Appliquer CropBox") as t:
        t.Start()
        target_view.SetSectionBox(cropbox)
        if target_view.ViewTemplateId != ElementId.InvalidElementId:
            target_view.ViewTemplateId = ElementId.InvalidElementId
        target_view.SetCategoryHidden(ElementId(BuiltInCategory.OST_Levels), True)              # Cache les niveaux dans la vue
        target_view.SetCategoryHidden(ElementId(BuiltInCategory.OST_VolumeOfInterest), True)    # Cache les zones de définition dans la vue
        t.Commit()

    revit.uidoc.ActiveView = target_view
    revit.uidoc.RefreshActiveView()


def get_crop_box_transformed(view):
    """Retourne la bounding box transformée pour une coupe."""
    bbox = view.CropBox
    if not bbox or not bbox.Enabled:
        return None

    # Appliquer la transformation au repère global
    transform = bbox.Transform
    min_pt = transform.OfPoint(bbox.Min)
    max_pt = transform.OfPoint(bbox.Max)

    transformed_box = BoundingBoxXYZ()
    transformed_box.Min = XYZ(
        min(min_pt.X, max_pt.X),
        min(min_pt.Y, max_pt.Y),
        min(min_pt.Z, max_pt.Z)
    )
    transformed_box.Max = XYZ(
        max(min_pt.X, max_pt.X),
        max(min_pt.Y, max_pt.Y),
        max(min_pt.Z, max_pt.Z)
    )
    return transformed_box
#####################################################################
#🚀🚀🚀🚀🚀----------------------------------------------LANCEMENT:
#####################################################################
def main():
    view = revit.active_view
    if not is_supported_view(view):
        #🚩🚧🚩-Alerte:
        forms.alert("Type de vue non supportée",
                    title="Erreur de format", 
                    sub_msg="lancer la commande dans un plan d'étage, de plafond, une vue en plan ou une coupe",
                    ok=True,
                    yes=False,
                    no=False,
                    exitscript=True,
                    warn_icon=False)
        return

    if is_viewplan(view):
        bbox = get_crop_box_from_view_range(view)
    else:
        bbox = get_crop_box_transformed(view)

    apply_cropbox_to_user3d(bbox)

main()
2 Likes

Thank you very much, this is exactly what I need, but it seems that it doesn’t work well on cross-sections that are not aligned with the X and Y coordinates.

Something going wrong with my transform function… wait for news, i’ll try to find a solution.

I made some modifications to your script, trying to replicate the functionality of Pangolin’s Magic 3D Box tool.

#### coding: utf8

__title__   = "STR_3D depuis la vue"
__author__  = "Stéphane ROSPARS DUPIN"
__version__ = "Version: 1.0"
__doc__ = """Version = 1.0
Date    = 2024.03.26
_____________________________________________________________________
Description:
Passe la vue 3D utilisateur avec une zone de coupe alignée sur la vue active.
_____________________________________________________________________
Mode d'emploi:

_____________________________________________________________________
Last update:
- [024.03.26] - 1.0 Création
_____________________________________________________________________
Contact et support:
- Stéphane ROSPARS DUPIN - AIA ANGERS
_____________________________________________________________________
To-Do:
-> Ø
_____________________________________________________________________"""
#####################################################################
#📚📚📚📚---------------------------------------------BIBLIOTHEQUES:
#####################################################################
import Autodesk
from Autodesk.Revit.DB import (
    Transaction, FilteredElementCollector, BuiltInCategory, 
    ViewType, View3D, BoundingBoxXYZ, XYZ, Transform, ElementId,
    PlanViewPlane, Outline, BoundingBoxIntersectsFilter
)
from Autodesk.Revit.UI.Selection import PickBoxStyle
from pyrevit import revit, HOST_APP, script, forms


#####################################################################
#*️⃣0️⃣1️⃣2️⃣------------------------------------------------VARIABLES:
#####################################################################
uidoc = __revit__.ActiveUIDocument
doc = uidoc.Document
app = __revit__.Application

#####################################################################
#🧬🧬🧬🧬------------------------------------------------FONCTIONS:
#####################################################################
def is_supported_view(view):
    return view.ViewType in [ViewType.FloorPlan, ViewType.EngineeringPlan,ViewType.CeilingPlan, ViewType.Section]
def is_viewplan(view):
    return view.ViewType in [ViewType.FloorPlan, ViewType.EngineeringPlan,ViewType.CeilingPlan]


def zoom_to_bbox(view, bbox):
    """Fait un zoom étendu sur une BoundingBox donnée dans une vue donnée."""
    outline = Outline(bbox.Min, bbox.Max)
    filter_bbox = BoundingBoxIntersectsFilter(outline)
    
    ids_in_box = FilteredElementCollector(doc, view.Id).WherePasses(filter_bbox).ToElementIds()
    
    if ids_in_box:
        revit.uidoc.ShowElements(ids_in_box)

def get_crop_box_from_view_range(view):
    """Construit une bounding box orientée à partir de la plage de vue (plans) ou fallback sur la bounding box brute."""
    #-----------  
    # Crear el estilo de selección para PickBox
    pboxS = Autodesk.Revit.UI.Selection.PickBoxStyle.Crossing

    # Seleccionar la caja con el estilo Directional
    pickedBox = uidoc.Selection.PickBox(pboxS, "Arrastra una caja de selección")

    # Imprimir las coordenadas de los puntos
    point1 = pickedBox.Min
    point2 = pickedBox.Max
    
    print(point1)
    print(point2)

    #-----------------    
    # Récupération de la bounding box actuelle
    #bbox_view = view.CropBox if view.CropBoxActive else view.get_BoundingBox(None)
    #if not bbox_view:
       # return None

    #transform = bbox_view.Transform  # Important pour l'orientation !

    # X/Y depuis la bbox (fallback plus stable que CurveLoop)
    #minX = bbox_view.Min.X
    #maxX = bbox_view.Max.X
    #minY = bbox_view.Min.Y
    #maxY = bbox_view.Max.Y

    # Lecture de la plage de vue
    view_range = view.GetViewRange()
    bottom_id = view_range.GetLevelId(PlanViewPlane.BottomClipPlane)
    top_id = view_range.GetLevelId(PlanViewPlane.TopClipPlane)

    bottom_level = revit.doc.GetElement(bottom_id) if bottom_id != ElementId.InvalidElementId else None
    top_level = revit.doc.GetElement(top_id) if top_id != ElementId.InvalidElementId else None
  

    bottom_offset = view_range.GetOffset(PlanViewPlane.BottomClipPlane)
    top_offset = view_range.GetOffset(PlanViewPlane.TopClipPlane)
    

    bottom_elev = bottom_level.ProjectElevation + bottom_offset if bottom_level else 00.00
    top_elev = top_level.ProjectElevation + top_offset if top_level else 9.84 #3metros = 9.84ft


    # Création de la nouvelle bounding box
    bbox = BoundingBoxXYZ()
    bbox.Min = XYZ(point1.X, point2.Y, bottom_elev)
    bbox.Max = XYZ(point2.X, point1.Y, top_elev)
    #bbox.Transform = transform  


    return bbox

# ------------------------------------------
# EXPAND / REORIENT BOUNDINGBOX
# ------------------------------------------
def reorient_bbox_to_world(bbox):
    """Transforme une BoundingBox orientée dans le repère local vers le repère monde."""
    local_min = bbox.Min
    local_max = bbox.Max
    corners_local = [XYZ(x, y, z) for x in [local_min.X, local_max.X]
                                  for y in [local_min.Y, local_max.Y]
                                  for z in [local_min.Z, local_max.Z]]
    corners_world = [bbox.Transform.OfPoint(p) for p in corners_local]
    min_x = min(p.X for p in corners_world)
    min_y = min(p.Y for p in corners_world)
    min_z = min(p.Z for p in corners_world)
    max_x = max(p.X for p in corners_world)
    max_y = max(p.Y for p in corners_world)
    max_z = max(p.Z for p in corners_world)
    new_bbox = BoundingBoxXYZ()
    new_bbox.Min = XYZ(min_x, min_y, min_z)
    new_bbox.Max = XYZ(max_x, max_y, max_z)
    new_bbox.Transform = Transform.Identity
    return new_bbox


def apply_cropbox_to_user3d(cropbox):
    user = str(HOST_APP.username)
    user_view_name = "{3D - " + user[0] + "." + user[1:] + "}"
    collector = FilteredElementCollector(revit.doc).OfCategory(BuiltInCategory.OST_Views)

    target_view = next((v for v in collector if v.Name == user_view_name and isinstance(v, View3D)), None)
    if not target_view: 
        user_view_name="{3D}"
        target_view=next((v for v in collector if v.Name == user_view_name and isinstance(v, View3D)), None)
    elif not target_view:
        #🚩🚧🚩-Alerte:
        forms.alert("❌ Vue 3D non trouvée❌",
                    title="Erreur vue utilisateur", 
                    sub_msg="**{}** non trouvée".format(user_view_name),
                    ok=True,
                    yes=False,
                    no=False,
                    exitscript=True,
                    warn_icon=False)
        return

    with Transaction(revit.doc, "Appliquer CropBox") as t:
        t.Start()
        target_view.SetSectionBox(cropbox)
        if target_view.ViewTemplateId != ElementId.InvalidElementId:
            target_view.ViewTemplateId = ElementId.InvalidElementId
        target_view.SetCategoryHidden(ElementId(BuiltInCategory.OST_Levels), True)              # Cache les niveaux dans la vue
        target_view.SetCategoryHidden(ElementId(BuiltInCategory.OST_VolumeOfInterest), True)    # Cache les zones de définition dans la vue
        t.Commit()

    revit.uidoc.ActiveView = target_view
    revit.uidoc.RefreshActiveView()


def get_crop_box_transformed(view):
    # Crear el estilo de selección para PickBox
    pboxS = Autodesk.Revit.UI.Selection.PickBoxStyle.Enclosing

    # Seleccionar la caja con el estilo Directional
    pickedBox = uidoc.Selection.PickBox(pboxS, "Arrastra una caja de selección")

    # Imprimir las coordenadas de los puntos
    point1 = pickedBox.Min
    point2 = pickedBox.Max
    print("----------")
    print(point1)
    print(point2)


    """Retourne la bounding box transformée pour une coupe."""
    bbox = view.CropBox
    if not bbox or not bbox.Enabled:
        return None

    # Appliquer la transformation au repère global
    transform = bbox.Transform
    min_pt = transform.OfPoint(bbox.Min)
    max_pt = transform.OfPoint(bbox.Max)
    print("----------")
    print(min_pt)
    print(max_pt)

    transformed_box = BoundingBoxXYZ()
    transformed_box.Min = XYZ(
        min(min_pt.X, max_pt.X),
        min(point1.Y, point2.Y),
        min(min_pt.Z, max_pt.Z)
    )
    transformed_box.Max = XYZ(
        max(min_pt.X, max_pt.X),
        max(point1.Y, point2.Y),
        max(point1.Z, point2.Z)
    )
    return transformed_box
#####################################################################
#🚀🚀🚀🚀🚀----------------------------------------------LANCEMENT:
#####################################################################
def main():
    view = revit.active_view
    if not is_supported_view(view):
        #🚩🚧🚩-Alerte:
        forms.alert("Type de vue non supportée",
                    title="Erreur de format", 
                    sub_msg="lancer la commande dans un plan d'étage, de plafond, une vue en plan ou une coupe",
                    ok=True,
                    yes=False,
                    no=False,
                    exitscript=True,
                    warn_icon=False)
        return

    if is_viewplan(view):
        bbox = get_crop_box_from_view_range(view)
    else:
        bbox = get_crop_box_transformed(view)

    apply_cropbox_to_user3d(bbox)

main()

I edit the functions:
def get_crop_box_from_view_range(view):

and
def get_crop_box_transformed(view):

To get the points for the BoundingBox with the Revit API function:
uidoc.Selection.PickBox

The tool is functional with floor plans and sections. However, there’s a bug in the floor plan view when dragging the rectangle from any direction other than top-left to bottom-right.
Apologies for any messy code, I’m still in the learning process and appreciate your patience!

1 Like

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.

3 Likes