Modify Wall Levels

Let's practice selection and working with parameters once more and create another Add-In together! Remember - Practice, Practice, Practice 😉 We will solve that annoying issue when we want to change wall levels, but don't want to change the geometry!

Modify Wall Levels

Let's practice selection and working with parameters once more and create another Add-In together! Remember - Practice, Practice, Practice 😉 We will solve that annoying issue when we want to change wall levels, but don't want to change the geometry!

Summary

Practice makes perfect!

Let's continue practicing Selection and Parameters once more! We will create another add-in together, applying the concepts we learned in Modules 3-4 to ensure a better understanding.

We are going to solve an issue of changing wall levels. It's really annoying attempt to modify wall levels, only to encounter a warning because the offset hasn't been adjusted correctly.

Let's take control back into our coding hands 🙌!

Tool Brainstorming

Firstly, let's break down the tool creation process into smaller steps:

  • 1️⃣ Get Selected Walls

  • 2️⃣ Get User Input (New Levels)

  • 3️⃣ Calculate New Offset to keep geometry

  • 4️⃣ Modify Wall Levels

These are the main steps we need to solve. Additionally, we will make sure that tool is prone to errors and has simple error handling.

So get ready and Happy Coding!

1️⃣ Get Selected Walls

Firstly, we need to select some walls. And there are many different options to do that. I want to make something very universal that has multiple selection options.

👇To get our selection, we will create a function that does the following:

💡 It might sound like a lot of steps to select elements, but it's nothing new for you! And also you will be able to reuse same logic for your future scripts as well 😉

So let's create a function that does all that!

from pyrevit import forms
from Autodesk.Revit.UI.Selection import ObjectType, ISelectionFilter

class Walls_ISelectionFilter(ISelectionFilter):
    def AllowElement(self, element):
        if type(element) == Wall:
            return True

def get_selected_walls():
    # Get Current Selection
    sel_el_ids = uidoc.Selection.GetElementIds()
    sel_elems  = [doc.GetElement(e_id) for e_id in sel_el_ids]

    # Filters Walls
    selected_walls = [el for el in sel_elems if type(el) == Wall]

    # Prompt PickObjects if No Walls Selected
    if not selected_walls:
        try:
            picked_refs    = uidoc.Selection.PickObjects(ObjectType.Element, Walls_ISelectionFilter())
            selected_walls = [doc.GetElement(ref) for ref in picked_refs]
        except:
            pass

    # Ensure Walls Selected
    if not selected_walls:
        forms.alert('No Walls Selected. \nPlease Try Again', exitscript=True)

    return selected_walls

And since we don't even have any arguments, we just call this function to get selected walls.

Let's preview our selection and print WallType Names.

# 1️⃣ Get Selected Walls
selected_walls = get_selected_walls()

#👀Preview Selection
for wall in selected_walls:
    wall_type      = wall.WallType
    wall_type_name = Element.Name.GetValue(wall_type)
    print('Wall Type: {}'.format(wall_type_name))

🤔 Also before going to the next step, try to test your selection from all angles!

  • Select before click the button.

  • Click without anything selected.

  • Select Nothing and click Finish.

  • Cancel selection all together.

Just think of all possible cases that your users might interact with the tool.

💀 Keep in mind that non-coders get terrified seeing red wall of text when they get an error. Even though it's harmless, they will try to avoid seeing that at all cost!

2️⃣ Get User Input

Once user has selected walls, we need to ask what is the desired level for Base/Top constraints. And for that we need to create a custom UI.

There are different ways to create a UI with python in Revit

  • Custom WPF - Highly customizable, but steep-learning curve!

  • pyrevit.forms - Simple to use but Limited forms.

  • rpw.FlexFox - 👈 That's what we will use

💡Also rpw is included in pyRevit by default!

📃rpw.FlexForm

rpw module has a bunch of prewritten functions that you might find useful, but FlexForm stands out the most. It allows you to create custom UI forms by combining different components in any order you want.

For Example:

You will be able to create as many Text/ComboBox/CheckBox inputs in a single form as you need. They will be stacked one after another.
And while this form is not the most customizable, it's very simple to use!

In our case, we will need to create 2 CheckBoxes and 2 ComboBoxes, so user can select desired levels for Base/Top constraints and also have an option to modify only Base, only Top or Both of them together!

💡Keep in mind that this form returns dictionary.

When you will be creating your components, it takes first argument as the key name in the dictionary. This will allow you to read the values in the end by specifying this key name!

def get_user_input():
    """Function to get user input.
    :return: dict of selected values.
    dict_keys = 'modify_wall_base' | 'base_level' | 'modify_wall_top' | 'top_level' """
    all_levels = FilteredElementCollector(doc).OfClass(Level).ToElements()
    dict_levels = {lvl.Name: lvl for lvl in all_levels}

    from rpw.ui.forms import (FlexForm, Label, ComboBox, TextBox, Separator, Button, CheckBox)
    components = [CheckBox('modify_base', 'Modify Base:'),
                  ComboBox('base_level', dict_levels),
                  CheckBox('modify_top', 'Modify Top:'),
                  ComboBox('top_level', dict_levels),
                  Separator(),
                  Button('Select')]
    form = FlexForm(__title__, components)
    form.show()

    if not form.values:
        forms.alert("No Levels Selected.\nPlease try Again", exitscript=True)

    return form.values

We just need to call our function name to create this form. And once the user finished selection, we will get a dictionary of all the user inputs.

Make sure you get your values using the same keys you provided in your components!

👇 Here is how I will get all user inputs from the form

# 2️⃣ Get User's Input (Create UI Form for Levels)
user_input = get_user_input()

new_base_level = user_input['base_level']
new_top_level  = user_input['top_level']
modify_base    = user_input['modify_base']
modify_top     = user_input['modify_top']
3️⃣ Calculate New Offsets

By this point we have selected walls, and we know what are the new Base/Top Levels.
So we can start calculating new offsets, while practicing working with parameters.

For this we will need to get the following parameters:

  • Base Level (WALL_BASE_CONSTRAINT)

  • Base Offset (WALL_BASE_OFFSET)

  • Top Level (WALL_HEIGHT_TYPE)

  • Top Offset (WALL_TOP_OFFSET)

  • Wall Height (WALL_USER_HEIGHT_PARAM)

💡 Inspect your elements with RevitLookup to find correct Built-In Parameter Names

for wall in selected_walls:


    # 3️⃣ Calculate New Offsets for New Levels

    #🔎 Get All Parameters
    p_base_level  = wall.get_Parameter(BuiltInParameter.WALL_BASE_CONSTRAINT)
    p_base_offset = wall.get_Parameter(BuiltInParameter.WALL_BASE_OFFSET)
    p_top_level   = wall.get_Parameter(BuiltInParameter.WALL_HEIGHT_TYPE)
    p_top_offset  = wall.get_Parameter(BuiltInParameter.WALL_TOP_OFFSET)
    p_wall_height = wall.get_Parameter(BuiltInParameter.WALL_USER_HEIGHT_PARAM)

Once we got these parameters, we can calculate Wall's Elevations.

For that we need to get Elevation of Wall's Levels and then include offsets. Then by subtracting these elevations, we will get a value that will be our new offset!

    #👉 Get values
    base_level    = doc.GetElement(p_base_level.AsElementId()) #type: Level
    base_offset   = p_base_offset.AsDouble()
    wall_height   = p_wall_height.AsDouble()

    #🧮 Calculate Elevations
    wall_base_elevation = base_level.Elevation + base_offset
    wall_top_elevation  = wall_base_elevation + wall_height

💡 Also notice that we haven't used Top Level / Top Offset parameter.

We used Wall Height instead, because it already provides us the necessary value. However, these parameters will be used later on when we will change Top Level and Offset.

4️⃣ Modify Base/Top Levels + Offsets

Now that we have everything we need, the last step is to modify the parameters with new values.

Since we've given the user an option to modify only Base/Top or Both, we need to make a few if statements.

We have modify_base and modify_top from our FlexForm, so let's check if they return True or False.

Then we can use Set method to change parameter value.

💡Make sure you set the parameter value with the correct StorageType.

If parameter is a Double, you need to provide you values as Double and so on… but in this case it's quite simple!


        # 4️⃣ Modify Base/Top Levels and offsets
        if modify_base:
            new_offset = wall_base_elevation - new_base_level.Elevation

            p_base_level.Set(new_base_level.Id)
            p_base_offset.Set(new_offset)

        if modify_top:
            new_offset = wall_top_elevation - new_top_level.Elevation

            p_top_level.Set(new_top_level.Id)
            p_top_offset.Set(new_offset)
🔓Add Transaction - How Allow Changes

As you remember, we have to use a Transaction every time we make any changes with the Revit API. This is great as it gives us a sense of security, knowing we can't mess up our projects.

So, create your transaction and make sure you make changes between Start and Commit statements.

💡Also make sure you keep your Transaction outside of the loops if possible. This will be a huge difference in the execution speed.

#🔓 Start Transaction
t = Transaction(doc, 'Modify Wall levels')
t.Start()

# Changes Here...

t.Commit()
✨Bonus Feature:

Also, as a bonus feature, we can think about work-sharing projects.

While automating something in Revit, you will often encounter warnings about elements being used by other users in the Work-Sharing project. Isn't it annoying?

So we can at least make a check to see if our elements are not OwnedByOtherUser. And for that we need to use the GetCheckoutStatus method from the WorksharingUtils Class.

# 👀 Check if Element is Taken (WorkSharing)
checkoutStatus = WorksharingUtils.GetCheckoutStatus(doc, wall.Id)
if checkoutStatus == CheckoutStatus.OwnedByOtherUser:
    print('[{}]Wall is Taken by Another User.'.format(wall.Id))
    continue
💻Final Code:

Let's put all these pieces together in a single code snippet:

# -*- coding: utf-8 -*-
__title__ = "04.07 - Change Wall Levels"
__doc__ = """Version = 1.0
Date    = 15.01.2024
_____________________________________________________________________
Description:
Tool to change Wall Constraint Levels while keeping the same geometry.

Happy Coding!
_____________________________________________________________________
Author: Erik Frits"""

# ╦╔╦╗╔═╗╔═╗╦═╗╔╦╗╔═╗
# ║║║║╠═╝║ ║╠╦╝ ║ ╚═╗
# ╩╩ ╩╩  ╚═╝╩╚═ ╩ ╚═╝ IMPORTS
# ==================================================
from Autodesk.Revit.DB import *
from pyrevit import forms
from Autodesk.Revit.UI.Selection import ObjectType, ISelectionFilter

# ╦  ╦╔═╗╦═╗╦╔═╗╔╗ ╦  ╔═╗╔═╗
# ╚╗╔╝╠═╣╠╦╝║╠═╣╠╩╗║  ║╣ ╚═╗
#  ╚╝ ╩ ╩╩╚═╩╩ ╩╚═╝╩═╝╚═╝╚═╝ VARIABLES
# ==================================================
doc   = __revit__.ActiveUIDocument.Document #type: Document
uidoc = __revit__.ActiveUIDocument
app   = __revit__.Application

# ╔═╗╦ ╦╔╗╔╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
# ╠╣ ║ ║║║║║   ║ ║║ ║║║║╚═╗
# ╚  ╚═╝╝╚╝╚═╝ ╩ ╩╚═╝╝╚╝╚═╝ FUNCTIONS
# ==================================================
class Walls_ISelectionFilter(ISelectionFilter):
    def AllowElement(self, element):
        if type(element) == Wall:
            return True

def get_selected_walls():
    # Get Current Selection
    sel_el_ids = uidoc.Selection.GetElementIds()
    sel_elems  = [doc.GetElement(e_id) for e_id in sel_el_ids]

    # Filters Walls
    selected_walls = [el for el in sel_elems if type(el) == Wall]

    # Prompt PickObjects if No Walls Selected
    if not selected_walls:
        try:
            picked_refs    = uidoc.Selection.PickObjects(ObjectType.Element, Walls_ISelectionFilter())
            selected_walls = [doc.GetElement(ref) for ref in picked_refs]
        except:
            pass

    # Ensure Walls Selected
    if not selected_walls:
        forms.alert('No Walls Selected. \nPlease Try Again', exitscript=True)

    return selected_walls

def get_user_input():
    """Function to get user input.
    :return: dict of selected values.
    dict_keys = 'modify_wall_base' | 'base_level' | 'modify_wall_top' | 'top_level' """
    all_levels = FilteredElementCollector(doc).OfClass(Level).ToElements()
    dict_levels = {lvl.Name: lvl for lvl in all_levels}

    from rpw.ui.forms import (FlexForm, Label, ComboBox, TextBox, Separator, Button, CheckBox)
    components = [CheckBox('modify_base', 'Modify Base:'),
                  ComboBox('base_level', dict_levels),
                  CheckBox('modify_top', 'Modify Top:'),
                  ComboBox('top_level', dict_levels),
                  Separator(),
                  Button('Select')]
    form = FlexForm(__title__, components)
    form.show()

    if not form.values:
        forms.alert("No Levels Selected.\nPlease try Again", exitscript=True)

    return form.values

# ╔╦╗╔═╗╦╔╗╔
# ║║║╠═╣║║║║
# ╩ ╩╩ ╩╩╝╚╝ Main
# ==================================================

# 1️⃣ Get Selected Walls
selected_walls = get_selected_walls()

# for wall in selected_walls:
#     wall_type      = wall.WallType
#     wall_type_name = Element.Name.GetValue(wall_type)
#     print('Wall Type: {}'.format(wall_type_name))

# 2️⃣ Get User's Input (Create UI Form for Levels)
user_input = get_user_input()

new_base_level = user_input['base_level']
new_top_level  = user_input['top_level']
modify_base    = user_input['modify_base']
modify_top     = user_input['modify_top']

#🔓 Start Transaction
t = Transaction(doc, 'Modify Wall levels')
t.Start()

for wall in selected_walls:
    try:
        # 👀 Check if Element is Taken (WorkSharing)
        checkoutStatus = WorksharingUtils.GetCheckoutStatus(doc, wall.Id)
        if checkoutStatus == CheckoutStatus.OwnedByOtherUser:
            print('[{}]Wall is Taken by Another User.'.format(wall.Id))
            continue

        # 3️⃣ Calculate New Offsets for New Levels

        #🔎 Get All Parameters
        p_base_level  = wall.get_Parameter(BuiltInParameter.WALL_BASE_CONSTRAINT)
        p_base_offset = wall.get_Parameter(BuiltInParameter.WALL_BASE_OFFSET)
        p_top_level   = wall.get_Parameter(BuiltInParameter.WALL_HEIGHT_TYPE)
        p_top_offset  = wall.get_Parameter(BuiltInParameter.WALL_TOP_OFFSET)
        p_wall_height = wall.get_Parameter(BuiltInParameter.WALL_USER_HEIGHT_PARAM)

        #👉 Get values
        base_level    = doc.GetElement(p_base_level.AsElementId()) #type: Level
        base_offset   = p_base_offset.AsDouble()
        wall_height   = p_wall_height.AsDouble()

        #🧮 Calculate Elevations
        wall_base_elevation = base_level.Elevation + base_offset
        wall_top_elevation  = wall_base_elevation + wall_height


        # 4️⃣ Modify Base/Top Levels and offsets
        if modify_base:
            new_offset = wall_base_elevation - new_base_level.Elevation

            p_base_level.Set(new_base_level.Id)
            p_base_offset.Set(new_offset)

        if modify_top:
            new_offset = wall_top_elevation - new_top_level.Elevation

            p_top_level.Set(new_top_level.Id)
            p_top_offset.Set(new_offset)

    except:
        import traceback
        print(traceback.format_exc(), wall.Id)




t.Commit()
#⌨️ Happy Coding!

HomeWork

💪Practice Working with Selection and Parameters!

Create a simple tool for another category of elements, to modify some parameters. We can already create a ton of tools by using the same logic as I showed you.

👇 Don't forget to share your ideas in the community. Together you will encourage each other to think about new tools, and how this can be used.

💬 Also, Feel Free to Ask for Help in the Discord to Create Your Tool!

⌨️ Happy Coding!

Questions:

How to create 100% custom forms?

How to create 100% custom forms?

Discuss the lesson:

P.S. This discord window is controlled with a bot and sometimes you might experience connection issues. If that happened, open Discord server with an app and look for this lesson's channel. Alternatively you could give it a try later in the browser.

Sorry for that, I am still figuring it out.
- Erik

P.S. This discord window is controlled with a bot and sometimes you might experience connection issues. If that happened, open Discord server with an app and look for this lesson's channel. Alternatively you could give it a try later in the browser.

Sorry for that, I am still figuring it out.
- Erik

© 2023-2024 EF Learn Revit API

© 2023-2024 EF Learn Revit API

© 2023-2024 EF Learn Revit API