View Sheet from View and Select All on Level

The number of Python and Dynamo oriented topics in the Revit API discussion forum is increasing. Here are some of them, and a final deprecated API clean-up:

The Building Coder Samples Clean

Before diving into the Python oriented topics, I'll mention in passing that the deprecated Revit API usage remaining after the initial migration to the Revit 2022 API has now been removed.

The deprecated API usage was caused by calls to the NewFloor and NewSlab methods.

The recent floor creation API clarification explained how to easily address this, and The Building Coder samples release 2022.0.151.4 implements the fix, cf. the diff to the previous release.

Retrieving all Elements on Level

Perry Lackowski jumped through several hoops to select all content on level and very kindly documented his progress and results to achieve this:

Question: I'm trying to put together a new script for a structural engineer. He wants to be able to quickly see all the elements that are linked to a particular Level of his choosing. I chose to break this into two parts:

Step 1 was more challenging than I realized, because some elements like cable trays store the associated Level in the ReferenceLevel instead of in the LevelId. Their LevelId returns -1 or an invalid element id.

Step 2 is where I'm struggling now. I use a FilteredElementCollector to find all matching LevelIds, but similar to step 1, this fails to include the cable trays in the resulting selection because they have a LevelId of -1.

Answer: Your explanation makes perfect sense, and also points towards the solution.

Just as you noticed in retrieving the level from the selected element, different elements store their level in different ways. Unfortunately, some do not store any level information directly at all. Those could be retrieved by determining their Z elevation and comparing that with the various level's Z coordinates.

Many elements provide a valid LevelId property, and you have used that property to retrieve the level from the selected element.

The cable trays apparently do not, and you have to use the ReferenceLevel property instead.

I assume that the ElementLevelFilter is also based on the LevelId property. Therefore, it will not retrieve the desired cable trays. For those, you can implement a second, separate, filtered element collector that first filters for cable trays, e.g., using their category or some other quick filter property. In a post-processing step, you could check that the value of their ReferenceLevel property matches the desired value.

These two separate filtered element collectors can be combined into one using a Boolean operation.

I used this technique to put together such combinations of filters to retrieve structural elements and MEP elements and their connectors, respectively.

You can check out The Building Coder topic group on filtering for elements to see many more examples.

Response: I didn't realize there was so much diversity in the way Revit handles the different type categories behind the scenes until I downloaded the RevitLookup snoop tool yesterday. For example, I see that Duct and Cable Tray are broken out into separate Duct and CableTray Objects, and they both use Reference Levels. But Duct Fittings and Cable Tray Fittings are saved under the same FamilyInstance object and they both use LevelIds. Is there a guide/roadmap for how different type categories map to different database objects? It sounds like I need to find out how each and every type category handles levels behind the scenes, then set up different filters to separate them ... which could be very time consuming.

Also (and perhaps this would be a better question to ask on the pyRevit forums) is there a better way to check for a null value on the LevelId parameter? I feel like converting the result to a string is not the right solution here.

Later: I'm beginning to wrap my head around the complexity of this problem. From the items I have snooped so far, here are my results:

Clearly, there's not a lot of consistency behind the scenes. I figured I'd take a look at the parameters next, and maybe filter based on those. Using the RevitAPI doc for BuiltInParameter enumeration, I'm seeing a lot of potential parameters that could house the level:

Admittedly, a lot of these built-in parameters sound like they correspond to families that I'm not looking for, but there are certainly quite a few contenders that may contain the information I need. How would I filter for these? I'm thinking I can create a list of all the parameters, then check for the parameters on each element and if I find a non-null parameter I can compare it to my selected level. But this would be a very slow filter. And how would I get a list of all the elements in the first place?

Later: OK, so I think I figured out the first part with this new method. Basically, it just searches for every parameter in the list, and if it finds one that doesn't equal -1, it will return it as the Element Id of the corresponding level. Next, I need to find a way to run this on every element in the project so I can compare the element ids. This feels like the slowest, most brute-force way to accomplish this, so I'm open to alternatives...

Later: Latest version, basically complete!

I discovered that some levels can't be retrieved through the get_Parameter method – they only appear in the LevelId and ReferenceLevel properties. But these methods don't exist for every element type, so I wrapped them in some Try/Except statements at the end of the level retrieval function.

I discovered a solution to the -1 issue as well. If you retrieve the element id and it's null, it means that level parameter doesn't exist for that object. But, if you retrieve an element id that equals -1, that means the parameter exists, but was never set. I believe the correct way to check for this is by comparing the element id to ElementId.InvalidElementId, like so:

  level_id.Compare(ElementId.InvalidElementId) == 1:

I also added some options so you can select the starting element before or after launching the script.

Unfortunately, I was forced to make a list of all the categories I want to search through, since I haven't found an easier way to filter down the FilteredElementCollector. I included maybe 30 of the more than 1000 categories, but it's not an exhaustive list, and there's a possibility I missed a few important ones that I'll discover later. I wish this page separated the 3D model categories from the rest, but alas.

"""
Selects all elements that share the same Reference Level as the selected element.

TESTED REVIT API: 2020.2.4

Author: Robert Perry Lackowski

"""

from Autodesk.Revit.DB import ElementLevelFilter, FilteredElementCollector
from Autodesk.Revit.DB import Document, BuiltInParameter, BuiltInCategory, ElementFilter, ElementCategoryFilter, LogicalOrFilter, ElementIsElementTypeFilter, ElementId
from Autodesk.Revit.Exceptions import OperationCanceledException
# from pyrevit import DB
doc = __revit__.ActiveUIDocument.Document
uidoc = __revit__.ActiveUIDocument

from rpw import ui
import sys

#Ask user to pick an object which has the desired reference level
def pick_object():
  from Autodesk.Revit.UI.Selection import ObjectType

  try:
    picked_object = uidoc.Selection.PickObject(ObjectType.Element, "Select an element.")
    if picked_object:
      return doc.GetElement(picked_object.ElementId)
    else:
      sys.exit()
  except:
    sys.exit()

def get_level_id(elem):

  BIPs = [
    BuiltInParameter.CURVE_LEVEL,
    BuiltInParameter.DPART_BASE_LEVEL_BY_ORIGINAL,
    BuiltInParameter.DPART_BASE_LEVEL,
    # BuiltInParameter.FABRICATION_LEVEL_PARAM,
    BuiltInParameter.FACEROOF_LEVEL_PARAM,
    BuiltInParameter.FAMILY_BASE_LEVEL_PARAM,
    BuiltInParameter.FAMILY_LEVEL_PARAM,
    BuiltInParameter.GROUP_LEVEL,
    BuiltInParameter.IMPORT_BASE_LEVEL,
    BuiltInParameter.INSTANCE_REFERENCE_LEVEL_PARAM,
    BuiltInParameter.INSTANCE_SCHEDULE_ONLY_LEVEL_PARAM,
    BuiltInParameter.LEVEL_PARAM,
    BuiltInParameter.MULTISTORY_STAIRS_REF_LEVEL,
    BuiltInParameter.PATH_OF_TRAVEL_LEVEL_NAME,
    BuiltInParameter.PLAN_VIEW_LEVEL,
    # BuiltInParameter.RBS_START_LEVEL_PARAM,
    BuiltInParameter.ROOF_BASE_LEVEL_PARAM,
    BuiltInParameter.ROOF_CONSTRAINT_LEVEL_PARAM,
    BuiltInParameter.ROOM_LEVEL_ID,
    BuiltInParameter.SCHEDULE_BASE_LEVEL_PARAM,
    BuiltInParameter.SCHEDULE_LEVEL_PARAM,
    BuiltInParameter.SLOPE_ARROW_LEVEL_END,
    # BuiltInParameter.SPACE_REFERENCE_LEVEL_PARAM,
    BuiltInParameter.STAIRS_BASE_LEVEL,
    BuiltInParameter.STAIRS_BASE_LEVEL_PARAM,
    BuiltInParameter.STAIRS_RAILING_BASE_LEVEL_PARAM,
    BuiltInParameter.STRUCTURAL_REFERENCE_LEVEL_ELEVATION,
    BuiltInParameter.SYSTEM_ZONE_LEVEL_ID,
    BuiltInParameter.TRUSS_ELEMENT_REFERENCE_LEVEL_PARAM,
    BuiltInParameter.VIEW_GRAPH_SCHED_BOTTOM_LEVEL,
    BuiltInParameter.VIEW_UNDERLAY_BOTTOM_ID,
    BuiltInParameter.WALL_BASE_CONSTRAINT,
    BuiltInParameter.WALL_SWEEP_LEVEL_PARAM
    # BuiltInParameter.ZONE_LEVEL_ID,
  ]

  level_id = None

  for BIP in BIPs:
    param = elem.get_Parameter(BIP)
    if param:
      # print "A common level parameter has been found:" + str(BIP)
      param_elem_id = param.AsElementId()
      if param_elem_id.Compare(ElementId.InvalidElementId) == 1:
        level_id = param_elem_id
        # print "match found on common level parameter " + str(BIP) + "Level ID: " + str(level_id)
        return level_id

  # print "No matching common level parameters found, checking for .LevelId"
  try:
    level_id = elem.LevelId
    if level_id.Compare(ElementId.InvalidElementId) == 1:
      # print "match found on .LevelId. Level ID: " + str(level_id)
      return level_id
  except:
    # print "No LevelId parameter on this element."
    pass

  # print "Still no matches. Try checking for .ReferenceLevel.Id"

  try:
    level_id = elem.ReferenceLevel.Id
    if level_id.Compare(ElementId.InvalidElementId) == 1:
      # print "match found on .ReferenceLevel.Id Level ID: " + str(level_id)      
      return level_id
  except:
    # print "No ReferenceLevel parameter on this element."
    pass

  # print "No matches found. Returning None..."
  return None

# print "get selected element, either from current selection or new selection"
selection = ui.Selection()

if selection:
  selected_element = selection[0]
else:
  selected_element = pick_object()

#print "Element selected: " + selected_element.Name

# print "Check if selected element is a Level and get its ID. If not, search through the parameters for the reference level."
if selected_element.Category.Name.Equals("Levels"):
  target_level_id = selected_element.Id
else:
  target_level_id = get_level_id(selected_element)
# print target_level_id

if target_level_id is not None:

  #poor attempts at filtering FECs. Not filtered enough - they contain far too many elements.
  #all_elements = FilteredElementCollector(doc).ToElements()
  #all_elements = FilteredElementCollector(doc).WherePasses(LogicalOrFilter(ElementIsElementTypeFilter( False ), ElementIsElementTypeFilter( True ) ) ).ToElements()

  #Create a filter. If this script isn't selecting the elements you want, it's possible the category needs to be added to this list.
  BICs = [
    BuiltInCategory.OST_CableTray,
    BuiltInCategory.OST_CableTrayFitting,
    BuiltInCategory.OST_Conduit,
    BuiltInCategory.OST_ConduitFitting,
    BuiltInCategory.OST_DuctCurves,
    BuiltInCategory.OST_DuctFitting,
    BuiltInCategory.OST_DuctTerminal,
    BuiltInCategory.OST_ElectricalEquipment,
    BuiltInCategory.OST_ElectricalFixtures,
    BuiltInCategory.OST_FloorOpening,
    BuiltInCategory.OST_Floors,
    BuiltInCategory.OST_FloorsDefault,
    BuiltInCategory.OST_LightingDevices,
    BuiltInCategory.OST_LightingFixtures,
    BuiltInCategory.OST_MechanicalEquipment,
    BuiltInCategory.OST_PipeCurves,
    BuiltInCategory.OST_PipeFitting,
    BuiltInCategory.OST_PlumbingFixtures,
    BuiltInCategory.OST_RoofOpening,
    BuiltInCategory.OST_Roofs,
    BuiltInCategory.OST_RoofsDefault,
    BuiltInCategory.OST_SpecialityEquipment,
    BuiltInCategory.OST_Sprinklers,
    BuiltInCategory.OST_StructuralStiffener,
    BuiltInCategory.OST_StructuralTruss,
    BuiltInCategory.OST_StructuralColumns,
    BuiltInCategory.OST_StructuralFraming,
    BuiltInCategory.OST_StructuralFramingSystem,
    BuiltInCategory.OST_StructuralFramingOther,
    BuiltInCategory.OST_StructuralFramingOpening,
    BuiltInCategory.OST_StructuralFoundation,
    BuiltInCategory.OST_Walls,
    BuiltInCategory.OST_Wire,
  ]

  category_filters = []

  for BIC in BICs:
    category_filters.Add(ElementCategoryFilter(BIC))

  final_filter = LogicalOrFilter(category_filters)

  #Apply filter to create list of elements
  all_elements = FilteredElementCollector(doc).WherePasses(final_filter).WhereElementIsNotElementType().WhereElementIsViewIndependent().ToElements()

  # print "Number of elements that passed collector filters:" + str(len(all_elements))

  selection.clear()

  for elem in all_elements:
    elem_level_id = get_level_id(elem)
    if elem_level_id == target_level_id:
      selection.add(elem)

  selection.update()

else:

  print "No level associated with element."

Many thanks to Perry for all his research and documentation of this work.

Spirit level

Get ViewSheet from View

We already discussed and documented how to retrieve a ViewSheet from a View based on the Revit API discussion forum thread get ViewSheet from View.

Erik Frits added a solution code snippet to that for Python developers:

You can use my snippet made with FilteredElementCollector and FilterStringRule:

Get ViewSheet from a given View

Just a snippet get_sheet_from_view(view) that will return you ViewSheet for a given View with the help of FilteredElementCollector and FilterStringRule.

# -*- coding: utf-8 -*-
__title__ = "Get sheet from View"
__author__ = "Erik Frits"

#>>>>>>>>>>>>>>>>>>>> IMPORTS
import clr, os
from Autodesk.Revit.DB import *

#>>>>>>>>>>>>>>>>>>>> VARIABLES
doc = __revit__.ActiveUIDocument.Document
uidoc = __revit__.ActiveUIDocument
app = __revit__.Application


#>>>>>>>>>>>>>>>>>>>> FUNCTIONS
def create_string_equals_filter(key_parameter, element_value, caseSensitive = True):
  """Function to create ElementParameterFilter based on FilterStringRule."""
  f_parameter         = ParameterValueProvider(ElementId(key_parameter))
  f_parameter_value   = element_value
  caseSensitive       = True
  f_rule              = FilterStringRule(f_parameter, FilterStringEquals(),
                        f_parameter_value, caseSensitive)
  return ElementParameterFilter(f_rule)

def get_sheet_from_view(view):
  #type:(View) -> ViewPlan
  """Function to get ViewSheet associated with the given ViewPlan"""

  #>>>>>>>>>> CREATE FILTER 
  my_filter = create_string_equals_filter(key_parameter=BuiltInParameter.SHEET_NUMBER,
    element_value=view.get_Parameter(BuiltInParameter.VIEWER_SHEET_NUMBER).AsString() )

  #>>>>>>>>>> GET SHEET
  return FilteredElementCollector(doc)
    .OfCategory(BuiltInCategory.OST_Sheets)
    .WhereElementIsNotElementType()
    .WherePasses(my_filter).FirstElement()

#>>>>>>>>>>>>>>>>>>>> MAIN
if __name__ == '__main__':

  #>>>>>>>>>> ACTIVE VIEW
  active_view = doc.ActiveView
  sheet     = get_sheet_from_view(active_view)

  #>>>>>>>>>> PRINT RESULTS
  if sheet:   print('Sheet Found: {} - {}'.format(sheet.SheetNumber, sheet.Name))
  else:     print('No sheet associated with the given view: {}'.format(active_view.Name))

Thanks to Erik for sharing this.

Fabrication Transaction in Dynamo

Lucas de Jong of WSP Canada clarified how to access a fabrication transaction in Dynamo, with invaluable support from Vlad Pavel of the Autodesk Revit development team, in the Revit API discussion forum thread on why 'Modify parameters' returns null for a newly created structural connection:

Question: I got it working in a Revit Addin, but I am trying to get it working inside of a Dynamo Zero-Touch-Node. Any reason why my GetFilerObject returns null inside a Zero-Touch-Node? Any help is greatly appreciated. My code is mostly (if not all) copied from the SDK sample.

Answer: You should create a fabrication transaction in order to open a steel object:

  using ( FabricationTransaction trans
    = new FabricationTransaction( doc, false, "Test" ) )
  {
    FilerObject filerObj = FilerObject.GetFilerObjectByHandle(asHandle);
    ...
  }

You can more examples on how to use it in the samples from the Revit SDK packages.

Response: Thank you @vlad.pavel.

Just last night I came across a comment of someone saying this, and have made some progress since. Unfortunately I am encountering a new problem when I set the readonly bool to 'true':

  new FabricationTransaction(doc, true, "Test")

I can now extract the parameter values. So, that is a win for now. Then, of course, I want to edit the parameters also, like it was done in the SDK sample.

When I leave the boolean on 'true' and try to write, load and update the UserAutoConstructionObject, Revit crashes.

When I set it to 'false', I get the error message that I cannot start a new transaction. I hope that when I solve that issue, I have made it to the finish. Please advise!

Please remember this is not in an external command, but in a Dynamo zero-touch-node.

Answer: Looks like in Dynamo for Revit there are special mechanisms that handle the Revit & Steel transactions. In zero touch nodes with the advance steel API, you should use the DocContext class from AdvanceSteelServices.dll located in the sub-folder under Revit.exe, in [Revit.exe path]\Addins\DynamoForRevit\Revit\nodes\steel-pkg\bin. For the pure Revit API, you should use RevitServices.Transactions.TransactionManager.Instance.EnsureInTransaction from RevitServices.dll.

So, for steel transactions in Dynamo for Revit, please replace

  using(FabricationTransaction trans = new Fabrication...)
  {
    trans.commit()
  } 

by

  using (var ctx = new Dynamo.Applications.AdvanceSteel.Services.DocContext())
  {
    ...
  }

The Dynamo-Revit code is open source; that is where I found how to use transactions in zero touch nodes. For example, the Wall class creates a wall. Unfortunately, we don't yet have any official documentation for this feature.

Response: @vlad.pavel I am so happy, you made my day! It worked perfectly!