The best way to upgrade your Revit files

The top-rated upgrade tool in the Autodesk App Store is back for Revit 2021.

Satisfied customers say:

“Our teams with large campus projects no longer groan when it is time for the annual upgrade.”

“There are other solutions floating around in cyberspace but this one is the Holy Grail.”

“I have been using the Bulk File Upgrader for years. The best and easiest add-in to use. I recommend this to anyone who needs to upgrade files.”

“This program is amazing. It works great in all ways that you would expect. “

Features include:

  • Select which file types to upgrade
  • Add suffix to new file names
  • Set “workset open” option (All, Editable, Last Viewed, Specify)
  • Speed the opening process by only opening specified worksets
  • Options to
    • Discard worksets
    • Delete backup folders
    • Relinquish editable worksets
    • Delete backup files
    • Remap RVT Links when saving to a new folder
    • Open files with Audit option

Your purchase helps support all the free API content and training material here at Boost Your BIM.

File Upgrade

Revit Lookup install for Revit 2021 & using Advanced Installer for easy MSI generation

Revit Lookup is a great open-source tool to help you better understand the data in your Revit model. I use it every day, and even if you aren’t writing Revit API code, sometimes it is still useful to get “under the hood” and better understand the data in your Revit model.

lookup

You can download the source and build it yourself, but that is not for everyone. So I created an MSI Installer that you can use to install the Lookup tool for Revit 2019, 2020, and 2021. I also submitted some changes to make it easier to build Revit Lookup for any release of Revit. Jeremy wrote a bit about it at The Building Coder.

I built the installer with Advanced Installer, a very powerful and easy to use tool for building your own installations developed by Caphyon. The drag-and-drop interface lets you quickly select the files you want to install and where the installation should place them on the target computer.

Advanced Installer has a freeware version with plenty of capabilities for many Revit API developers. If you want to support them and get even more features there are three different licenses that you can purchase.

installer

 

Innovate & Improve by Learning The Revit API

“We see this an opportunity to find new ways to innovate, improve our process and keep this economy moving.” – Wendy Rogers, LPA, Chief Executive Officer

Boost Your BIM and LPA have worked together on great projects to improve their Revit processes, and Wendy is right that we all should keep innovating while staying healthy and safe.

Hundreds of people have gained valuable skills through the Boost Your BIM Revit API video course. You save $ and I make more $ if you use the links below:

(Udemy is making these coupons expire each month, so the links will be updated as needed)

review 3review2review1

part1

part2

materials

 

 

Revit Lookup for Revit 2020

Revit Lookup is a great (and free) tool to learn more about what’s in your Revit models. If you want to look at source code, deal with GitHub, and learn more about it, go to https://github.com/jeremytammik/RevitLookup

If you just want to install it for Revit 2017-2020, go to https://drive.google.com/open?id=1lnhx9ngyTZHi-hMMby-4a5K8NyWIFtV5

Get total length of multiple lines & units conversion

This post on the Revit Ideas forum inspired a new lecture on the Boost Your BIM Revit API course at Udemy. It is a great example of how just a few lines of code (17, to be exact) can be written in just a few minutes to do something useful.

Override Dimension Text (part 2)

Building on our last post, here is the finished tool with UI that allows you to modify the dimension text on a single or multi-segment dimension by selecting the dimension, not the dimension text.

FREE Installer: https://drive.google.com/open?id=16_oZYpfXjXcSaYe_IOv-K0ivR4ebsMLr

add additional way to get to Dimension Text dialog (to edit Dimension’s text)

In the Revit Ideas forum it was asked if it can be possible to access this dialog to change dimension text without having to select the dimension’s text itself. Either the text has been overridden with an invisible, empty character, a view break was used, or for some other reason it isn’t simple to select the dimension’s text. Or you want to avoid the problem of moving the dimension text by mistake when you try to click it.

Fortunately, the API gives you access to most everything in this dialog (maybe not “show label in view” and “segment dimension leader visibility”). The code is below to handle a dimension with a single segment (code for the Dialog form is not included) and it could also be enhanced to work with a multi-segment dimension if the form listed the text of each dimension segment so you could specify which segment to modify.

Is there interest in a Udemy lecture showing how to code the form? Or posting it as an app in the Autodesk store?

public void dimText()
{
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = this.ActiveUIDocument;
    Dimension dim = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element, "Select dimension")) as Dimension;
    if (dim.NumberOfSegments > 1)
    {
        TaskDialog.Show("Error", "Can only run when the dimension has a single segment");
        return;
    }
    
    string above = "";
    string below = "";
    string prefix = "";
    string suffix = "";
    string replaceWithText = "";
    
    using (FormDimText form = new FormDimText(dim))
    {
        form.ShowDialog();
        if (form.DialogResult == System.Windows.Forms.DialogResult.Cancel)
            return;
        above = form.getAbove();
        below = form.getBelow();
        prefix = form.getPrefix();
        suffix = form.getSuffix();
        replaceWithText = form.getReplace();
    }
    
    using (Transaction t = new Transaction(doc, "Dim Text"))
    {
        t.Start();

        dim.Above = above;
        dim.Below = below;
        if (replaceWithText == null)
        {
            dim.Prefix = prefix;
            dim.Suffix = suffix;    
        }
        else
        {
            dim.ValueOverride = replaceWithText;
        }

        t.Commit();
    }
}

New Revit API online course – Materials

Want to learn more about the Revit API for Materials? This is one of the areas where Autodesk has done a lot of great work over the past few years making much more functionality accessible to the API developer. But some of it can be tricky to figure out. So don’t waste time struggling with it, take this new course instead to quickly get up to speed.

Here are a few recent reviews from the Boost Your BIM introductory course on the Revit API:

“great way to learn a lot about how to program the Revit API”

“just exactly what I need to scale up”

“Great course, well structured, good ongoing updates for changes in versions”

If there are any material related topics that you don’t see listed in the curriculum, leave a comment so we can consider adding more lectures about what you want to learn.

Image-O-Matic is back! And Free!

Animate your Revit model by changing family parameters, displacement set offsets, or phases. Use it to test your family content by varying parameter values or to wow your colleagues with amazing animations. This popular tool now supports Revit 2019. You can download it from the Autodesk App Store here

Find that labeled dimension

Have you ever gotten lost while searching and searching in a complex family for a labeled dimension? You know there is a dimension somewhere labeled “Jamb Width” or “Head Height” or whatever but you don’t know in what view that dimension lives?

Here is a macro that will find a dimension based on the name of its labeled parameter, open the dimension’s view, select the dimension and zoom to it.

dims

Where is Jamb Width?

dim 2

Found it!

public void findLabelView()
{
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = this.ActiveUIDocument;
    foreach (Dimension dim in new FilteredElementCollector(doc)
        .OfClass(typeof(Dimension))
        .Cast<Dimension>())
    {
        FamilyParameter fp = null;
        try
        {
            fp = dim.FamilyLabel;
        }
        catch
        {
            continue;
        }
        
        if (fp != null && fp.Definition.Name == "Jamb Width")
        {
            uidoc.Selection.SetElementIds(new List<ElementId> { dim.Id});
            View ownerview = doc.GetElement(dim.OwnerViewId) as View;
            uidoc.ActiveView = ownerview;
            UIView uiview = uidoc.GetOpenUIViews().FirstOrDefault(q => q.ViewId == dim.OwnerViewId);
            BoundingBoxXYZ bbox = dim.get_BoundingBox(ownerview);
            uiview.ZoomAndCenterRectangle(bbox.Min, bbox.Max);
            break;
        }
    }
}

 

New in 2019.1 – Attached Detail Group API

The new 2019.1 release has added API for attached detail groups! Thanks Autodesk!

For example:

  • GetAvailableAttachedDetailGroupTypeIds returns the attached detail groups of a group or group type
  • HideAllAttachedDetailGroups & HideAttachedDetailGroups to hide some or or all of a model group’s attached detail groups in a specific view
  • ShowAttachedDetailGroups to show an attached detail group on a model group in a specific view

attached detail 1

On a technical note, GetAvailableAttachedDetailGroupTypeIds returns a C# “set”, a data structure that can store certain values, without any particular order, and no repeated values. A couple good resources to learn more about sets are:

public void GetAvailableAttachedDetailGroupTypeIds()
{
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = this.ActiveUIDocument;
    Group modelGroup = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element, "Select Model Group")) as Group;
    HashSet attachedDetailIds = modelGroup.GetAvailableAttachedDetailGroupTypeIds() as HashSet;
    TaskDialog.Show("Attached detail groups", string.Join(Environment.NewLine, attachedDetailIds.Select(q => doc.GetElement(q).Name)));
}
public void HideDetailGroup()
{
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = this.ActiveUIDocument;
    Group modelGroup = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element, "Select Model Group")) as Group;
    Group detailGroup = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element, "Select detail group to hide")) as Group;
    using (Transaction t = new Transaction(doc, "hide detail groups"))
    {
        t.Start();
        modelGroup.HideAttachedDetailGroups(doc.ActiveView, detailGroup.GroupType.Id);
        t.Commit();
    }
}
public void ShowDetailGroup()
{
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = this.ActiveUIDocument;
    Group modelGroup = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element, "Select Model Group")) as Group;
    Element detailGroupType = new FilteredElementCollector(doc).OfClass(typeof(GroupType))
        .OfCategory(BuiltInCategory.OST_IOSAttachedDetailGroups)
        .FirstOrDefault(q => q.Name == "Door Dimensions A6");
        
    using (Transaction t = new Transaction(doc, "show detail group"))
    {
        t.Start();
        modelGroup.ShowAttachedDetailGroups(doc.ActiveView, detailGroupType.Id);
        t.Commit();
    }
}

 

#BILTNA2018 Wish granted: Select detail lines by Line Type

It would be nice if we could right click on a specific linetype and “select all instances” in view instead of having to select everything and then filter them out.

public void selectAllLineStyle()
{
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = this.ActiveUIDocument;
    DetailLine line = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element, "Select detail line")) as DetailLine;
    
    // if you want only the lines in the current view, add "doc.ActiveView.Id" to the FilteredElementCollector
    List<CurveElement> lines = new FilteredElementCollector(doc)
        .OfClass(typeof(CurveElement))
        .OfCategory(BuiltInCategory.OST_Lines)
        .Where(q => q is DetailLine)
        .Cast<CurveElement>()
        .Where(q => q.LineStyle.Id == line.LineStyle.Id)
        .ToList();
    uidoc.Selection.SetElementIds(lines.Select(q => q.Id).ToList());
}

 

#BILTNA Wish 5: Isolate with Fade

Isolate command should fade and lock all other objects instead of hiding them completely. What use can i find in seeing an object out of context? I can’t compare it visually to anything. If i want to modify it and can’t relate any modification to any context (because it’s hidden)

public void isolateFade()
{
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = this.ActiveUIDocument;
    ElementId elementIdToIsolate = null;
    try
    {
        elementIdToIsolate = uidoc.Selection.PickObject(ObjectType.Element, "Select element or ESC to reset the view").ElementId;
    }
    catch
    {
        
    }
    
    OverrideGraphicSettings ogsFade = new OverrideGraphicSettings();
    ogsFade.SetSurfaceTransparency(50);
    ogsFade.SetSurfaceForegroundPatternVisible(false);
    ogsFade.SetSurfaceBackgroundPatternVisible(false);
    ogsFade.SetHalftone(true);
    
    OverrideGraphicSettings ogsIsolate = new OverrideGraphicSettings();
    ogsIsolate.SetSurfaceTransparency(0);
    ogsIsolate.SetSurfaceForegroundPatternVisible(true);
    ogsIsolate.SetSurfaceBackgroundPatternVisible(true);
    ogsIsolate.SetHalftone(false);
    
    using (Transaction t = new Transaction(doc, "Isolate with Fade"))
    {
        t.Start();
        foreach (Element e in new FilteredElementCollector(doc, doc.ActiveView.Id).WhereElementIsNotElementType())
        {
            if (e.Id == elementIdToIsolate || elementIdToIsolate == null)
                doc.ActiveView.SetElementOverrides(e.Id, ogsIsolate);
           else
                doc.ActiveView.SetElementOverrides(e.Id, ogsFade);
        }
        t.Commit();
    }
}

#BILTNA Wish 4: Spell check for schedules

Why doesn’t Revit check spelling in schedules? No good reason that I know of, so here’s an investigation into how it might be done with the API. There’s also some discussion of JSON deserialization and communicating with the Bing Spell Check web API.

spellcheck

public void scheduleSpellCheck()
{
    Document doc = this.ActiveUIDocument.Document;
    ViewSchedule viewSchedule = doc.ActiveView as ViewSchedule;
    ViewSheet viewSheet = doc.ActiveView as ViewSheet;
    if (viewSchedule == null && viewSheet == null)
    {
        TaskDialog.Show("Error", "Active a schedule or sheet before running this command");
        return;
    }
    SectionType sectionType = SectionType.Body;
    List<string> scheduleText = new List<string>();
    
    List schedules = new List();
    if (viewSheet != null)
    {
        schedules = new FilteredElementCollector(doc).OfClass(typeof(ScheduleSheetInstance)).Cast()
            .Where(q => q.OwnerViewId == viewSheet.Id)
            .Select(q => doc.GetElement(q.ScheduleId) as ViewSchedule)
            .Where(q => !q.IsTitleblockRevisionSchedule).ToList();
    }
    else
    {
        schedules.Add(viewSchedule);
    }
    
    foreach (ViewSchedule vs in schedules)
    {
        scheduleText.Add(vs.Name);
        for (int i = 0; i < getRowColumnCount(vs, sectionType, true); i++)
        {
            for (int j = 0; j < getRowColumnCount(vs, sectionType, false); j++)
            {
                try
                {
                    scheduleText.Add(vs.GetCellText(sectionType, i, j));
                }
                catch (Exception ex)
                {
                    
                }
            }
        }
        SpellCheck(string.Join(" - ", scheduleText));
    }
}

 static async void SpellCheck(string text)
{
    HttpClient client = new HttpClient();
    client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", key);

    // The following headers are optional, but it is recommended they be treated as required.
    // These headers help the service return more accurate results.
    //client.DefaultRequestHeaders.Add("X-Search-Location", ClientLocation);
    //client.DefaultRequestHeaders.Add("X-MSEdge-ClientID", ClientId);
    //client.DefaultRequestHeaders.Add("X-MSEdge-ClientIP", ClientIp);

    HttpResponseMessage response = new HttpResponseMessage();
    string uri = host + path + params_;

    List<KeyValuePair<string, string>> values = new List<KeyValuePair<string, string>>();
    values.Add(new KeyValuePair<string, string>("text", text));

    using (FormUrlEncodedContent content = new FormUrlEncodedContent(values))
    {
        content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
        response = await client.PostAsync(uri, content);
    }

    string client_id;
    IEnumerable<string> header_values = new List<string>();
    if (response.Headers.TryGetValues("X-MSEdge-ClientID", out header_values))
    {
        client_id = header_values.First();
        Console.WriteLine("Client ID: " + client_id);
    }

    string results = await response.Content.ReadAsStringAsync(); 
    TaskDialog.Show("JSON", JsonPrettyPrint(results));
    FlaggedTokenList flaggedTokenList = new JavaScriptSerializer().Deserialize(results);
    List<string> output = new List<string>();
    foreach (FlaggedToken flaggedToken in flaggedTokenList.flaggedTokens)
    {
        List<string> suggestions = flaggedToken.suggestions.Select(q => q.suggestion.ToString()).ToList();
        if (suggestions.Count == 0)
            continue;
        output.Add(flaggedToken.token + " : " + string.Join(",", suggestions));
    }
    TaskDialog.Show("output", string.Join(Environment.NewLine, output));
}

static string host = "https://api.cognitive.microsoft.com";
static string path = "/bing/v7.0/spellcheck?";

// For a list of available markets, go to:
// https://docs.microsoft.com/rest/api/cognitiveservices/bing-autosuggest-api-v7-reference#market-codes
static string params_ = "mkt=en-US&mode=proof";

// NOTE: Replace this example key with a valid subscription key.
static string key = "d46d0d618a7343dabe8a4355e9d30187";
         
class FlaggedTokenList
{
    public List flaggedTokens { get; set; }
}

class FlaggedToken
{
    public string token { get; set; }
    public List suggestions { get; set; }
}    

class Suggestion
{
    public string suggestion { get; set; }
    public double score { get; set; }
}

        
private int getRowColumnCount(ViewSchedule view, SectionType sectionType, bool countRows)
{
    int ctr = 1;
    // loop over the columns of the schedule
    while (true)
    {
        try // GetCellText will throw an ArgumentException is the cell does not exist
        {
            if (countRows)
                view.GetCellText(sectionType, ctr, 1);
            else
                view.GetCellText(sectionType, 1, ctr);
            ctr++;
        }
        catch (Autodesk.Revit.Exceptions.ArgumentException)
        {
            return ctr;
        }
    }
}    

static string JsonPrettyPrint(string json)
{
    if (string.IsNullOrEmpty(json))
    {
        return string.Empty;
    }

    json = json.Replace(Environment.NewLine, "").Replace("\t", "");

    StringBuilder sb = new StringBuilder();
    bool quote = false;
    bool ignore = false;
    char last = ' ';
    int offset = 0;
    int indentLength = 3;

    foreach (char ch in json)
    {
        switch (ch)
        {
            case '"':
                if (!ignore) quote = !quote;
                break;
            case '\\':
                if (quote && last != '\\') ignore = true;
                break;
        }

        if (quote)
        {
            sb.Append(ch);
            if (last == '\\' && ignore) ignore = false;
        }
        else
        {
            switch (ch)
            {
                case '{':
                case '[':
                    sb.Append(ch);
                    sb.Append(Environment.NewLine);
                    sb.Append(new string(' ', ++offset * indentLength));
                    break;
                case '}':
                case ']':
                    sb.Append(Environment.NewLine);
                    sb.Append(new string(' ', --offset * indentLength));
                    sb.Append(ch);
                    break;
                case ',':
                    sb.Append(ch);
                    sb.Append(Environment.NewLine);
                    sb.Append(new string(' ', offset * indentLength));
                    break;
                case ':':
                    sb.Append(ch);
                    sb.Append(' ');
                    break;
                default:
                    if (quote || ch != ' ') sb.Append(ch);
                    break;
            }
        }
        last = ch;
    }

    return sb.ToString().Trim();
}

#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();
    }
}

#BILTNA2018 Wish granted: Align a straight wall with a curved grid

The problem:

The solution:

public void alignWallToCurvedGrid()
{
    UIDocument uidoc = this.ActiveUIDocument;
    Document doc = this.ActiveUIDocument.Document;
    Wall wall = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element, "Select wall")) as Wall;
    Grid grid = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element, "Select grid")) as Grid;
    Curve gridCurve = grid.Curve;
    Line wallLine = (((LocationCurve)wall.Location).Curve as Line);
    XYZ wallDirection = wallLine.Direction;
    XYZ wallPerpendicular = XYZ.BasisZ.CrossProduct(wallDirection);
    
    XYZ intersection0 = intersect(wallLine.GetEndPoint(0), wallPerpendicular, gridCurve);
    XYZ intersection1 = intersect(wallLine.GetEndPoint(1), wallPerpendicular, gridCurve);
    XYZ intersectionMid = intersect(wallLine.Evaluate(0.5, true), wallPerpendicular, gridCurve);
    
    Arc arc = Arc.Create(intersection0, intersection1, intersectionMid);
    
    List wallChildren = new FilteredElementCollector(doc)
             .OfClass(typeof(FamilyInstance))
             .Cast()
        .Where(q => q.Host.Id == wall.Id).ToList();
    
    using (Transaction t = new Transaction(doc, "Wall to Curve"))
    {
        t.Start();
        Wall newWall = Wall.Create(doc, arc, wall.LevelId, false);
        newWall.WallType = doc.GetElement(wall.GetTypeId()) as WallType;
        foreach (FamilyInstance fi in wallChildren)
        {
            XYZ fiPoint = (fi.Location as LocationPoint).Point;
            XYZ fiIntersection = intersect(fiPoint, wallPerpendicular, gridCurve);
            if (fiIntersection != null)
            {
                FamilyInstance fiNew = doc.Create.NewFamilyInstance(fiIntersection, 
                fi.Symbol,
                newWall,
                doc.GetElement(fi.LevelId) as Level,
                StructuralType.NonStructural);
                
                if (fi.FacingFlipped)
                    fiNew.flipFacing();
                if (fi.HandFlipped)
                    fiNew.flipHand();
            }
        }
        doc.Delete(wall.Id);
        t.Commit();
    }
}

private XYZ intersect(XYZ point, XYZ direction, Curve curve)
{
    Line unbound = Line.CreateUnbound(new XYZ(point.X, point.Y, curve.GetEndPoint(0).Z), direction);
    IntersectionResultArray ira = null;
    unbound.Intersect(curve, out ira);
    if (ira == null)
    {
        TaskDialog td = new TaskDialog("Error");
        td.MainInstruction = "no intersection";
        td.MainContent = point.ToString() + Environment.NewLine + direction.ToString();
        td.Show();
            
        return null;
    }
    IntersectionResult ir = ira.Cast().First();
    return ir.XYZPoint;
}