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