#BILTNA2018 Wish 3 Granted: Level remap to allow safe level deletion

The problem

If you delete a level in Revit, it deletes all objects on that level – no warning is given.

There should be a safe way of finding objects that are assigned to that level and reassigning them to a different level.

Perhaps Revit could automatically assign them to the level below?

Its a real problem if you create too many levels at the beginning of a project, you cant then delete some as you’ll lose information.

The solution – an API command to remap elements so they no longer reference the selected level
public void levelRemap()
{
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = this.ActiveUIDocument;
    Level level = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element, "Select level")) as Level;
    
    Level levelBelow = new FilteredElementCollector(doc)
        .OfClass(typeof(Level))
        .Cast<Level>()
        .OrderBy(q => q.Elevation)
        .Where(q => q.Elevation <= level.Elevation)
        .FirstOrDefault();
    
    if (levelBelow == null)
    {
        TaskDialog.Show("Error", "No level below " + level.Elevation);
        return;
    }
    
    List<string> paramsToAdjust = new List<string> { "Base Offset", "Sill Height", "Top Offset"};
    
    List<Element> elements = new FilteredElementCollector(doc).WherePasses(new ElementLevelFilter(level.Id)).ToList();
    using (Transaction t = new Transaction(doc, "Level Remap"))
    {
        t.Start();
        foreach (Element e in elements)
        {
            foreach (Parameter p in e.Parameters)
            {
                if (p.StorageType != StorageType.ElementId || p.IsReadOnly)
                    continue;
                if (p.AsElementId() != level.Id)
                    continue;
                
                double elevationDiff = level.Elevation - levelBelow.Elevation;
                
                p.Set(levelBelow.Id);
                
                foreach (string paramName in paramsToAdjust)
                {
                    Parameter pToAdjust = e.LookupParameter(paramName);
                    if (pToAdjust != null && !pToAdjust.IsReadOnly)
                        pToAdjust.Set(pToAdjust.AsDouble() + elevationDiff);
                }
            }
        }
        t.Commit();
    }
}

10 thoughts on “#BILTNA2018 Wish 3 Granted: Level remap to allow safe level deletion

  1. Great Harry. I have develop something similar years ago for my company. Our app display a list
    of elements attached to each level and a list of levels to choose one and change the elements. There are some special cases for certains categories that require more code to make it work I all.

  2. Hi Harry, Just testing this out on the advanced sample project and a few errors pop up. In addition it seems that floors at the selected level are getting deleted

    • I added “Height Offset From Level” to the paramsToAdjust list. I’m starting to see how this works. There’d be an issue with elements associated to two levels ie top and bottom. Columns for axample have a bottom level and a top level. Walls can be attached to a top level. How would you adjust the script to account for this?

      • Hi Alexis – You are right that some additional work would be needed to take this proof-of-concept to the next level (pun intended!). For example, the Top Offset should only be set if the Top Level (for columns) or Top Constraint (for walls) is set.

  3. Hi Harry!
    I’m trying to apply de code into r2019.2 but have no luck; what goes prior the “public partial class ThisApplication” ?

    I tried:

    public partial class ThisDocument
    {

    }

    But again, no luck :c

  4. There is something i’m quoting that the classes can’t see, i don’t know what…

    public partial class ThisDocument
    {

    public partial class ThisApplication
    {

    public void levelRemap()
    {
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = this.ActiveUIDocument;
    Level level = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element, “Select level”)) as Level;

    Level levelBelow = new FilteredElementCollector(doc)
    .OfClass(typeof(Level))
    .Cast()
    .OrderBy(q => q.Elevation)
    .Where(q => q.Elevation <= level.Elevation)
    .FirstOrDefault();

    if (levelBelow == null)
    {
    TaskDialog.Show("Error", "No level below " + level.Elevation);
    return;
    }

    List paramsToAdjust = new List { “Base Offset”, “Sill Height”, “Top Offset”};

    List elements = new FilteredElementCollector(doc).WherePasses(new ElementLevelFilter(level.Id)).ToList();
    using (Transaction t = new Transaction(doc, “Level Remap”))
    {
    t.Start();
    foreach (Element e in elements)
    {
    foreach (Parameter p in e.Parameters)
    {
    if (p.StorageType != StorageType.ElementId || p.IsReadOnly)
    continue;
    if (p.AsElementId() != level.Id)
    continue;

    double elevationDiff = level.Elevation – levelBelow.Elevation;

    p.Set(levelBelow.Id);

    foreach (string paramName in paramsToAdjust)
    {
    Parameter pToAdjust = e.LookupParameter(paramName);
    if (pToAdjust != null && !pToAdjust.IsReadOnly)
    pToAdjust.Set(pToAdjust.AsDouble() + elevationDiff);
    }
    }
    }
    t.Commit();
    }
    }
    }
    }
    }

  5. When i run the script it remaps to the lowest level in the project. how can i modify the script so it moves to the next lowest level in the sequence for instance instead of from level 4 to 1 it goes from level 4 to 3? thank you

    • Hi – This is the code that finds the level below. Right now it sorts the levels by elevation, finds only the level’s with an elevation less than the selected level, and then takes the first level in the resulting list. If you’d like to learn more about how this works, you might start with
      https://www.c-sharpcorner.com/article/linq-for-beginners/ and search the internet for more information about
      c# linq orderby
      c# linq where

      Level levelBelow = new FilteredElementCollector(doc)
      .OfClass(typeof(Level))
      .Cast()
      .OrderBy(q => q.Elevation)
      .Where(q => q.Elevation <= level.Elevation)
      .FirstOrDefault();

  6. Thank you! I chose to do .OrderByDescending. And under .Where I changed “<= level.Elevation" to " < level.Elevation" that achieved the results i was looking for

    Level levelBelow = new FilteredElementCollector(doc)
    .OfClass(typeof(Level))
    .Cast()
    .OrderByDescending(q => q.Elevation)
    .Where(q => q.Elevation < level.Elevation)
    .FirstOrDefault();

Leave a comment