MallerComplexity Family tool goes open-source

During RTCNA Boost Your BIM collaborated with Kelly Cone and Matt Mason to create a tool to measure complexity of Revit families. It is named in honor of Aaron Maller who makes some of the most sophisticated families around.

It scores each family and its top-level nested families based on the # of family parameters and # of operators (‘>’, ‘<‘, ‘+’, ‘-‘, ‘*’, ‘/’) in parameter formulas.

Thinking that others might like to continue the development of this tool, Boost Your BIM has created a public repository for the code and looks forward to seeing the enhancements that others might implement.

Git repository: https://bitbucket.org/BoostYourBIM/mallercomplexity

Discussion at Revit Forum: http://www.revitforum.org/architecture-general-revit-questions/30805-revit-maller-complexity-setup-wizard.html

 

#RTCNA wish granted – Design Option Set info

Rabi asked

I am curious if you have figured a way to get the Design Options Set from Revit API

Here’s what it seems we can do with the parameters OPTION_SET_ID and PRIMARY_OPTION_ID

Capture

public void getDesignOptionSet()
{
    Document doc = this.ActiveUIDocument.Document;
    List<ElementId> setIds = new List<ElementId>();
    foreach (DesignOption dopt in new FilteredElementCollector(doc).OfClass(typeof(DesignOption)).Cast<DesignOption>())
    {
        ElementId setId = dopt.get_Parameter(BuiltInParameter.OPTION_SET_ID).AsElementId();
        if (!setIds.Contains(setId))
            setIds.Add(setId);
    }
    
    string data = "";
    foreach (ElementId id in setIds)
    {
        Element e = doc.GetElement(id);
        data += e.Name + " - " + doc.GetElement(e.get_Parameter(BuiltInParameter.PRIMARY_OPTION_ID).AsElementId()).Name + Environment.NewLine;
    }
    
    TaskDialog.Show("data",data);
}

#RTCNA wish almost granted – clear keynote when duplicating types

Clearing out my RTC API Wishlist Inbox… (do you have more wishes? RTCEUR is coming in 3 months)

Nicklas asks

Is it possible to create a macro that resets keynote when a family is duplicated?

The code and video below shows how Dynamic Model Update can be used to clear the Keynote parameter’s value any time a new Element Type is added to the file. But the problem with this is that it does not distinguish the different ways that a new Element Type can be added.

You can duplicate a type (the scenario that Nicklas wants to capture) but it also triggers when you load a family (a scenario that probably should not clear the keynote values for the just loaded families). The solution to this might include subscribing to the DocumentChanged event and using the GetTransactionNames Method to differentiate between a Duplicate and Load Family.

public void addTrigger()
{
    Application app = this.Application;
    MyUpdater udpater = new MyUpdater(app.ActiveAddInId);
    UpdaterRegistry.RegisterUpdater(udpater, true);
    UpdaterRegistry.AddTrigger(udpater.GetUpdaterId(), 
                               new ElementClassFilter(typeof(ElementType)), Element.GetChangeTypeElementAddition());
}

public class MyUpdater : IUpdater
{
    static AddInId m_appId;
    static UpdaterId m_updaterId;
    public MyUpdater(AddInId id)
    {
        m_appId = id;
        m_updaterId = new UpdaterId(m_appId, new Guid("FB1BA6B2-4C06-42d4-97C1-D1B4EB593EFA"));
    }
    public void Execute(UpdaterData data)
    {
        Document doc = data.GetDocument();
        try
        {
            foreach (ElementId id in data.GetAddedElementIds())
            {
                ElementType etype = doc.GetElement(id) as ElementType;
                if (etype == null)
                    continue;
                Parameter p = etype.get_Parameter(BuiltInParameter.KEYNOTE_PARAM);
                if (p == null)
                    continue;
                p.Set("");
            }
            
        }
        catch (Exception ex)
        {
            string s = ex.Message;
            TaskDialog td = new TaskDialog("MyUpdater Exception");
            td.MainInstruction = ex.Message;
            td.MainContent = ex.StackTrace;
            td.Show();
        }
    }
    public string GetAdditionalInformation() { return ""; }
    public ChangePriority GetChangePriority() { return ChangePriority.FloorsRoofsStructuralWalls; }
    public UpdaterId GetUpdaterId() { return m_updaterId; }
    public string GetUpdaterName() { return "MyUpdater"; }
}

#RTCNA wish granted (sort of) – slab edges on all floor edges

An API wish from Michael

Creating slab edges programmatically for all edges of a slab. The UI does not allow you to pick all edges of a slab in some cases. For example, on a slab edge that is sloping and curving in plan. Is there a way to get around this in the API by hard setting the reference curve?

It turns out that the limitation on sloping/curving edges still exists when we use the API, but in any case, below is the API code to create slab edges on all edges of the floor’s top face.

public void slabEdge()
{
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = this.ActiveUIDocument;
    Floor floor = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element)) as Floor;
    Options options = new Options();
    options.ComputeReferences = true;
    Solid solid = null;
    foreach (Autodesk.Revit.DB.GeometryObject geomObj in floor.get_Geometry(options))
    {
        Autodesk.Revit.DB.Solid s = geomObj as Autodesk.Revit.DB.Solid;
        if (null != s)
        {
            if (s.Faces.Size > 0)
            {
                solid = s;
                break;
            }
        }
     }
        Face topFace = null;
        double bigZ = 0;
        foreach (Face f in solid.Faces)
        {
            PlanarFace pf = f as PlanarFace;
            if (pf == null)
                continue;
            if (pf.FaceNormal.Z > bigZ)
            {
                bigZ = pf.FaceNormal.Z;
                topFace = f;
            }
        }
        
        SlabEdgeType slabType = new FilteredElementCollector(doc).OfClass(typeof(SlabEdgeType)).Cast<SlabEdgeType>().FirstOrDefault();
        int ctr = 0;
        using (Transaction t = new Transaction(doc, "slab edges"))
        {
            t.Start();
            foreach (EdgeArray ea in topFace.EdgeLoops)
            {
                foreach (Edge e in ea)
                {
                    try
                    {
                     SlabEdge se = doc.Create.NewSlabEdge(slabType, e.Reference);
                     se.HorizontalFlip();
                     ctr++;
                    }
                    catch
                    {    
                    }
                }
            }
            t.Commit();
        }
        TaskDialog.Show("Info", ctr + " slab edges created");
}

#RTCNA2016 Wish Granted – Load Group from RVT

bd5cents sent in an API wish to:

Load Group in to project from external Group file. .rvt file.

This could be particularly useful if you have a folder full of Group RVTs and want to load them all, or if you want to create a dialog box where the user can select multiple groups to load.

Unfortunately, the API does not have a simple Document.LoadGroup() method, so we have to do a bit more work.

public void LoadModelGroup()
{
    string groupFile = @"C:\Users\harry_000\Documents\MyGroup.rvt";
    Application app = this.Application;
    
    Document doc = this.ActiveUIDocument.Document;
    
    // find elements to copy
    // to find the model elements in the group file,
    // create a 3d view and then get all the elements in that view
    Document docGroup = app.OpenDocumentFile(groupFile);
    ICollection<ElementId> toCopy = null;
    using (Transaction t = new Transaction(docGroup, "temp"))
    {
        t.Start();
        View3D view = View3D.CreateIsometric(docGroup, new FilteredElementCollector(docGroup)
                             .OfClass(typeof(ViewFamilyType))
                             .Cast<ViewFamilyType>()
                             .FirstOrDefault(q => q.ViewFamily == ViewFamily.ThreeDimensional).Id);
        toCopy = new FilteredElementCollector(docGroup, view.Id).ToElementIds();
        t.RollBack();
    }
    
    using (Transaction t = new Transaction(doc, "Copy Group"))
    {
        t.Start();
        
        // paste the elements from the group file
        ICollection<ElementId> newIds = ElementTransformUtils.CopyElements(docGroup, toCopy, doc, Transform.Identity, new CopyPasteOptions());
        
        // group the newly pasted elements
        Group group = doc.Create.NewGroup(newIds);
        
        // set the name of the group type
        group.GroupType.Name = Path.GetFileNameWithoutExtension(groupFile);
        
        // delete the instance of the group
        doc.Delete(group.Id);
        
        t.Commit();
    }
    
    docGroup.Close(false);
}

#RTCNA2016 Wish 2 Granted (part 2)

The previous post showed how to create lines in an elevation view to visualize the view range of a plan view. Now let’s see how to let the user move those lines in the elevation view and update the plan view’s range.

public void setViewRangeByLine()
{
    string planName = "Level 1 View Range Test";

    Document doc = this.ActiveUIDocument.Document;
    
    ViewPlan viewPlan = new FilteredElementCollector(doc)
        .OfClass(typeof(ViewPlan))
        .Cast<ViewPlan>()
        .FirstOrDefault(q => q.Name == planName);
    if (viewPlan == null)
        return;
    
    CurveElement bottomCurve = new FilteredElementCollector(doc, doc.ActiveView.Id)
        .OfClass(typeof(CurveElement))
        .Cast<CurveElement>().FirstOrDefault(q => q.LineStyle.Name == "Bottom Clip Plane");
    if (bottomCurve == null)
        return;
    
    double bottomZ = bottomCurve.GeometryCurve.GetEndPoint(0).Z;

    PlanViewRange range = viewPlan.GetViewRange();
    using (Transaction t = new Transaction(doc, "Set view range"))
    {
        t.Start();
        range.SetOffset(PlanViewPlane.BottomClipPlane, bottomZ);
        viewPlan.SetViewRange(range);
        t.Commit();
    }
    
}

#RTCNA2016 Wish 2 Granted! (part one)

Timothy had this API wish:

View Ranges in Section to show graphically like a space in section (with Interior checked on) . Allow me to drag the box up/down to level if desired otherwise keep “Level Above”.

Here is part one – creating lines in an elevation view to show the view range of a plan view.

public void showViewRange()
{
    string planName = "Level 1 View Range Test";
    
    Document doc = this.ActiveUIDocument.Document;
    
    ViewPlan viewPlan = new FilteredElementCollector(doc)
        .OfClass(typeof(ViewPlan))
        .Cast<ViewPlan>()
        .FirstOrDefault(q => q.Name == planName);
    if (viewPlan == null)
        return;
    
    PlanViewRange range = viewPlan.GetViewRange();
    Level bottomLevel = doc.GetElement(range.GetLevelId(PlanViewPlane.BottomClipPlane)) as Level;
    Level topLevel = doc.GetElement(range.GetLevelId(PlanViewPlane.TopClipPlane)) as Level;
    
    Category bottomClipLineStyle = doc.Settings.Categories.Cast<Category>().FirstOrDefault(q => q.Id.IntegerValue == (int)BuiltInCategory.OST_Lines)
        .SubCategories.Cast<Category>().FirstOrDefault(q => q.Name == "Bottom Clip Plane");

    Category topClipLineStyle = doc.Settings.Categories.Cast<Category>().FirstOrDefault(q => q.Id.IntegerValue == (int)BuiltInCategory.OST_Lines)
        .SubCategories.Cast<Category>().FirstOrDefault(q => q.Name == "Top Clip Plane");
    
    using (Transaction t = new Transaction(doc, "Make View Range Line"))
    {
        t.Start();
        if (bottomLevel != null)
        {
            double bottomOffset = range.GetOffset(PlanViewPlane.BottomClipPlane);
            double z = bottomLevel.Elevation + bottomOffset;
            DetailCurve bottomLine = doc.Create.NewDetailCurve(doc.ActiveView, Line.CreateBound(new XYZ(0,0,z), new XYZ(30,0,z)));
            if (bottomClipLineStyle != null)
            {
                bottomLine.LineStyle = bottomClipLineStyle.GetGraphicsStyle(GraphicsStyleType.Projection);
            }
        }
        
        if (topLevel != null)
        {
            double topOffset = range.GetOffset(PlanViewPlane.TopClipPlane);
            double z = topLevel.Elevation + topOffset;
            DetailCurve topLine = doc.Create.NewDetailCurve(doc.ActiveView, Line.CreateBound(new XYZ(0,0,z), new XYZ(30,0,z)));
            if (topClipLineStyle != null)
            {
                topLine.LineStyle = topClipLineStyle.GetGraphicsStyle(GraphicsStyleType.Projection);
            }
        }
        t.Commit();
    }
    
}

#RTCNA2016 Wish 1 Granted!

Timothy had this API wish:

In Activate View event, If active workset contains “XX”, show task dialog warning user they are about to draw on an incorrect workset. We lock out users from “XX Shared Levels and Grids”, “XX Scope Boxes”, and “XX Links”


public void registerEvent()
{
    Application app = this.Application;
    UIApplication uiapp = new UIApplication(app);
    uiapp.ViewActivated += new EventHandler<ViewActivatedEventArgs>(uiapp_ViewActivated);
}

public void uiapp_ViewActivated(object sender, ViewActivatedEventArgs e)
{
    Document doc = e.Document;
    string activeWorksetName = GetActiveWorkset(doc).Name;
    if (activeWorksetName.Contains("XX"))
        TaskDialog.Show("Warning", "Do not create elements in 'XX' workset: " + activeWorksetName);
}

public Workset GetActiveWorkset(Document doc)
{
    // Get the workset table from the document
    WorksetTable worksetTable = doc.GetWorksetTable();
    // Get the Id of the active workset
    WorksetId activeId = worksetTable.GetActiveWorksetId();
    // Find the workset with that Id 
    Workset workset = worksetTable.GetWorkset(activeId);
    return workset;
}

What are your #RTCNA2016 API Wishes?

It’s that time of year again. Another Revit Technology Conference is up and running (or maybe walking to avoid a Phoenix heat-stroke) and Boost Your BIM is here to grant your API wishes!

Here are some examples of wishes that have been granted in the past. Please send your new wishes for interesting and useful ways the API might be able to help you make Revit better. Wishes can be tweeted to @BoostYourBIM or left as comments to this post

Total length of multiple lines

There’s been a bunch of discussion in the Autodesk forum about how to get the total length of multiple elements.

http://forums.autodesk.com/t5/revit-architecture/how-to-determine-the-total-length-of-multiple-lines/td-p/3297855

Here’s an API solution that works with lines, walls, beams, conduit, or anything else with a “Length” parameter

Capture

public void lineLength()
{
    double length = 0;
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = this.ActiveUIDocument;
    ICollection<ElementId> ids = uidoc.Selection.GetElementIds();
    foreach (ElementId id in ids)
    {
        Element e = doc.GetElement(id);
        Parameter lengthParam = e.get_Parameter(BuiltInParameter.CURVE_ELEM_LENGTH);
        if (lengthParam == null)
            continue;
        length += lengthParam.AsDouble();
    }
    string lengthWithUnits = UnitFormatUtils.Format(doc.GetUnits(), UnitType.UT_Length, length, false, false);
    TaskDialog.Show("Length", ids.Count + " elements = " + lengthWithUnits);
}

getting Dimension Segment data

Question: Can we use the API to automatically add up all of the dimension segments in a single string of dimensions? I want to use it to check the sum of a string of dimensions against an overall dimension for coordination purposes.

Answer:

Capture

public void DimensionSegments()
{
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = this.ActiveUIDocument;
    Dimension dim = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element)) as Dimension;
    double total = 0;
    string segmentsWithText = "";
    string segmentsNoText = "";
    string segmentValues = "";
    foreach (DimensionSegment dimSeg in dim.Segments)
    {
        total += (double)dimSeg.Value;
        
        segmentsWithText += dimSeg.ValueString + Environment.NewLine;
        
        string segmentString = dimSeg.ValueString;
        if (dimSeg.Above != null && dimSeg.Above != "")
            segmentString = segmentString.Replace(dimSeg.Above,"");
        if (dimSeg.Prefix != null && dimSeg.Prefix != "")
            segmentString = segmentString.Replace(dimSeg.Prefix,"");
        if (dimSeg.Suffix != null && dimSeg.Suffix != "")
            segmentString = segmentString.Replace(dimSeg.Suffix,"");
        if (dimSeg.Below != null && dimSeg.Below != "")
            segmentString = segmentString.Replace(dimSeg.Below,"");
        segmentsNoText += segmentString.TrimStart(' ') + Environment.NewLine;
    
        segmentValues += dimSeg.Value + Environment.NewLine;
    }
    TaskDialog.Show("Total Dimension", 
                    "Sum of lengths: " + total + Environment.NewLine + Environment.NewLine +
                    "Segment text:" + Environment.NewLine + segmentsWithText + Environment.NewLine + 
                    "Segments no text:" + Environment.NewLine + segmentsNoText + Environment.NewLine + 
                    "Segment values:" + Environment.NewLine + segmentValues);
}
        

Filter Rule data – where is it hiding?

It is in there, but you’ve got to dig into some sub-classes and static functions.

This doesn’t implement every case, hopefully is enough to shed some light on how it works.

public void ListFilters()
{
    Document doc = this.ActiveUIDocument.Document;
    foreach (ParameterFilterElement pfe in new FilteredElementCollector(doc).OfClass(typeof(ParameterFilterElement)).Cast<ParameterFilterElement>())
    {        
        string ruleData = "";                    
        string categories = "";
            
        foreach (ElementId catid in pfe.GetCategories())
        {
            categories += doc.Settings.Categories.get_Item(((BuiltInCategory)catid.IntegerValue)).Name + ",";
        }
                 
        foreach (FilterRule rule in pfe.GetRules())
        {
            string comparator = "";
            string ruleValue = "";

            if (rule is FilterDoubleRule)
            {
                FilterDoubleRule fdr = rule as FilterDoubleRule;
                
                if (fdr.GetEvaluator().GetType().Equals(typeof(FilterNumericLess)))
                    comparator = "<";
                else if (fdr.GetEvaluator().GetType().Equals(typeof(FilterNumericGreater)))
                    comparator = ">";
                
                ruleValue = fdr.RuleValue.ToString();

            }
            else if (rule is FilterStringRule)
            {
                FilterStringRule fsr = rule as FilterStringRule;
                
                if (fsr.GetEvaluator().GetType().Equals(typeof(FilterStringBeginsWith)))
                    comparator = "starts with";
                else if (fsr.GetEvaluator().GetType().Equals(typeof(FilterStringEndsWith)))
                    comparator = "ends with";
                else if (fsr.GetEvaluator().GetType().Equals(typeof(FilterStringEquals)))
                    comparator = "=";
                else if (fsr.GetEvaluator().GetType().Equals(typeof(FilterStringContains)))
                    comparator = "contains";
                
                ruleValue = fsr.RuleString;
            }
            else if (rule is FilterIntegerRule)
            {
                FilterIntegerRule fir = rule as FilterIntegerRule;
                
                if (fir.GetEvaluator().GetType().Equals(typeof(FilterNumericEquals)))
                    comparator = "=";
                else if (fir.GetEvaluator().GetType().Equals(typeof(FilterNumericGreater)))
                    comparator = ">";
                
                // some parameters store an integer but the UI shows a text string
                // this text comes from the value of an enum such as WallFunction
                if (((BuiltInParameter)ParameterFilterElement.GetRuleParameter(rule).IntegerValue) == BuiltInParameter.FUNCTION_PARAM)
                {
                    ruleValue = ((WallFunction)fir.RuleValue).ToString();
                }
                else
                    ruleValue = fir.RuleValue.ToString();
            }
            
            string paramName = "";
            if (ParameterFilterElement.GetRuleParameter(rule).IntegerValue < 0)
                paramName = LabelUtils.GetLabelFor((BuiltInParameter)ParameterFilterElement.GetRuleParameter(rule).IntegerValue);
            else
                paramName = doc.GetElement(ParameterFilterElement.GetRuleParameter(rule)).Name;

            
            ruleData += "'" + paramName + "' " +
                comparator + " " +
                "'" + ruleValue.ToString() + "'" +
                Environment.NewLine;
        }
        TaskDialog td = new TaskDialog("Rule");
        td.MainInstruction = "Filter name: " + pfe.Name;
        td.MainContent = "Categories: " + categories + Environment.NewLine + Environment.NewLine + ruleData;
        td.Show();
    }
}

Revit Lookup 2017 Installer

If you want to experience the joys of Revit Lookup (the interactive Revit BIM database exploration tool) on Revit 2017 without downloading the source code and compiling it yourself, you can download an installer for it at https://drive.google.com/open?id=0BwszsfY3OsZHSG5yaGdhejcyNkk

Capture

Code Signing For Revit 2017

In 2017, Autodesk really wants developers to sign their DLLs (http://thebuildingcoder.typepad.com/blog/2016/04/whats-new-in-the-revit-2017-api.html#2.4)

If you don’t, Revit will show this scary dialog on startup

not signed

Dealing with this was a fairly annoying process, so to hopefully make it easier for others, here is what I did.

  1. Bought a 5 year certificate from http://codesigning.ksoftware.net/ for $365
  2. Sent them a bunch of documentation to prove that I am who I say I am (including signing up for a free listing at http://www.yellowpages.com/allston-ma/mip/boost-your-bim-526332700?lid=526332700)
  3. Got my “certificate” and generated a PFX file (if you have questions about this step, post in the comments and I will add more info)
  4. Added this to the Visual Studio post build event
    “C:\Program Files (x86)\Windows Kits\8.0\bin\x64\signtool.exe” sign /f “C:\Users\harry_000\Documents\Boost Your BIM\BoostYourBIM.pfx” /t “http://timestamp.comodoca.com/authenticode&#8221; /p <password> /v $(TargetPath)

The result is that the DLL has a signature as shown below.

properties

 

Find Column Base Offset Values

For someone who asked: “I’m trying to get the offset values. How should I proceed?”

public void columns()
{
    Document doc = this.ActiveUIDocument.Document;
    FilteredElementCollector collector = new FilteredElementCollector(doc)
        .OfClass(typeof(FamilyInstance))
        .WhereElementIsNotElementType()
        .OfCategory(BuiltInCategory.OST_StructuralColumns);
    foreach (Element e in collector)
    {
        Parameter baseOffsetParameter = e.get_Parameter(BuiltInParameter.FAMILY_BASE_LEVEL_OFFSET_PARAM);    
        double baseOffsetValue = baseOffsetParameter.AsDouble();
    }
}    

Transform that Family Instance Geometry

Oh, if I had a nickel for every time I forgot (and later remembered) that geometry of family instances needs to be transformed like this…

Transform transform = null;
if (elem is FamilyInstance)
{
    FamilyInstance fi = elem as FamilyInstance;
    transform = fi.GetTransform();
}
                    
PlanarFace pf = elem.GetGeometryObjectFromReference(r) as PlanarFace;
if (pf == null)
    continue;
XYZ norm = pf.FaceNormal;
if (transform != null)
{
    norm = transform.OfVector(norm);
}

Honored to be #RTCEUR Top 10 Speaker #3

Thank you to everyone who attended my class “Get the Data Out: Calculating & Computing What Revit Won’t Give You” last month in Budapest at RTC Europe. I’m glad you enjoyed the course and thanks for the high marks you gave the course. There were 50+ speakers at RTC Europe 2015 and it is an honor to be voted #3!

Quick Tip: Module name should not be a Class name

If you create a macro module named “Wall” and then do this:

namespace Wall
{
    [Autodesk.Revit.Attributes.Transaction(Autodesk.Revit.Attributes.TransactionMode.Manual)]
    [Autodesk.Revit.DB.Macros.AddInId("B1079B97-D3BA-430F-9109-F492B7F2891F")]
    public partial class ThisApplication
    {
        public void test()
        {
            Document doc = this.ActiveUIDocument.Document;
            Element e = new FilteredElementCollector(doc).OfClass(typeof(FamilyInstance)).FirstOrDefault();
        }

Everything will be OK. But if you try to use the Revit API class “Wall” in your code, the compiler will be confused because the module is named “Wall” and you will get an error like below.
So keep those module names unique and different!

Capture