The last #RTCEUR #Revit API Wish – Parts Want Levels

Here’s the last wish granted! RVT Links are used to model typical floors and parts are created from the walls and floors in the link instances. Here is how to use the API to set a user-created “Level” parameter for each part based on the name of the level in the host RVT nearest to the lowest point in the geometry of each part.

parts

public void setPartLevel()
{
    Document doc = this.ActiveUIDocument.Document;
    using (Transaction t = new Transaction(doc, "Set Part Levels"))
    {
        t.Start();
        foreach (Part part in new FilteredElementCollector(doc).OfClass(typeof(Part)))
        {
            Parameter levelParam = part.Parameters.Cast<Parameter>().FirstOrDefault(q => q.Definition.Name == "Level");
            if (levelParam == null)
                return;
            
            double minZ = double.PositiveInfinity;    
            foreach (Solid s in part.get_Geometry(new Options()))
            {
                foreach (Face f in s.Faces)
                {
                    foreach (EdgeArray ea in f.EdgeLoops)
                    {
                        foreach (Edge e in ea)
                        {
                            Curve c = e.AsCurve();
                            if (c.GetEndPoint(0).Z < minZ)
                                minZ = c.GetEndPoint(0).Z;
                            if (c.GetEndPoint(1).Z < minZ)
                                minZ = c.GetEndPoint(1).Z;
                        }
                    }
                }
            }
            Level level = new FilteredElementCollector(doc).OfClass(typeof(Level)).Cast<Level>().OrderBy(q => Math.Abs(q.Elevation - minZ)).FirstOrDefault();
            levelParam.Set(level.Name);
        }
        t.Commit();
    }
}

Journal time-stamps at a set interval for another #RTCEUR #Revit API wish

Joel wished for “a time stamp added to journal files. I’m thinking something like an addin or macro that just notes the current time at specified intervals. The use would be for tracking performance – what processes take a long time, and how much how much time.”

It is so great that C# has the Timer class to make this easy – https://www.dotnetperls.com/timer

time-stamp

Application app = null;
public void JournalCommentTimer()
{
    app = this.Application;
    UIApplication uiapp = new UIApplication(app);
    double seconds = 10;
    Timer timer = new Timer(seconds * 1000);
    timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
    timer.Enabled = true;
}

void timer_Elapsed(object sender, ElapsedEventArgs e)
{
    app.WriteJournalComment("New Timestamp", true);
}

Add this to your file to compile the code above:
using System.Timers;

Purge unused materials for another #RTCEUR API wish

Yesterday I looked at how to purge unused families and family types. Purging materials needs a different approach because they are not used directly in the project but are referenced by other objects.

Here is an approach that uses the DocumentChanged event, rolling back Transaction Groups, and other good things do find materials that can be deleted without elements being modified. It takes a bit of time to run because it is deleting all materials and undoing the deletions that modify other elements.

material-purge

public static int modifiedByDeleteMaterial = 0;
public static bool checkForPurgeMaterials = false;
public static Document _doc = null;
public static string materialName = "";
public void purgeMaterials()
{
    Document doc = this.ActiveUIDocument.Document;
    _doc = doc;
    Application app = doc.Application;
    app.DocumentChanged += documentChanged_PurgeMaterials;
    List<Element> materials = new FilteredElementCollector(doc).OfClass(typeof(Material)).ToList();
    string deletedMaterials = "";
    int unusedMaterialCount = 0;
    foreach (Element material in materials)
    {
        modifiedByDeleteMaterial = 0;
        materialName = material.Name + " (id " + material.Id + ")";
        using (TransactionGroup tg = new TransactionGroup(doc, "Delete Material: " + materialName))
        {
            tg.Start();
            using (Transaction t = new Transaction(doc, "delete material"))
            {
                t.Start();
                checkForPurgeMaterials = true;
                doc.Delete(material.Id);
                
                // commit the transaction to trigger the DocumentChanged event
                t.Commit();
            }
            checkForPurgeMaterials = false;
            
            if (modifiedByDeleteMaterial == 1)
            {
                unusedMaterialCount++;
                deletedMaterials += materialName + Environment.NewLine;
                tg.Assimilate();
            }
            else // rollback the transaction group to undo the deletion
                tg.RollBack();
        }
    }
    
    TaskDialog td = new TaskDialog("Info");
    td.MainInstruction = "Deleted " + unusedMaterialCount + " materials";
    td.MainContent = deletedMaterials;
    td.Show();

    app.DocumentChanged -= documentChanged_PurgeMaterials;
}

private static void documentChanged_PurgeMaterials(object sender, Autodesk.Revit.DB.Events.DocumentChangedEventArgs e)
{
    // do not check when rolling back the transaction group
    if (!checkForPurgeMaterials)
    {
        return;
    }
    
    List<ElementId> deleted = e.GetDeletedElementIds().ToList();
    List<ElementId> modified = e.GetModifiedElementIds().ToList();
    
    // for debugging
    string s = "";
    foreach (ElementId id in modified)
    {
        Element modifiedElement = _doc.GetElement(id);
        s += modifiedElement.Category.Name + " " + modifiedElement.Name + " (" +  id.IntegerValue + ")" + Environment.NewLine;
    }
    //TaskDialog.Show("d", materialName + Environment.NewLine + "Deleted = " + deleted.Count + ", Modified = " + modified.Count + Environment.NewLine + s);
    
    // how many elements were modified and deleted when this material was deleted?
    // if 1, then the material is unused and should be deleted
    modifiedByDeleteMaterial = deleted.Count + modified.Count;
}

Making change (or select pipe lengths) at #RTCEUR

Building on yesterday’s post about pipe splitting, you may want a semi-intelligent way to choose quantities of pre-cut lengths to use when you break apart a long element into fabricated pieces.

Fortunately, this is the same challenge as how to make change for a given amount of money. You have a total amount (of money, length of pipe, etc) and a defined set of options (quarter, nickels, dimes, 6 and 10 foot pieces of pipe, etc) to use to get to the total. Many computer science students have probably tackled this problem and there are various solutions on the web.

bills-and-change-by-zoofytheji

The approach below uses recursion which is always exciting and adds a wrinkle that change-making doesn’t include. I added the idea of a “flexible length” and a minimum flexible length. It is possible to cut pipe into non-uniform lengths, but there is a minimum length that should not be allowed.

Some sample output for different lengths:

coins

static List<double> amounts = new List<double>();
static List<List<double>> results = new List<List<double>>();
static double minFlexibleDistance = 0;
static double goal = 0;
public void makeChange()
{
     amounts.Clear();
     
     // lengths that can be combined to reach the goal length
    amounts.Add(4);
    amounts.Add(6);
    amounts.Add(10);
    
    // this is the shortest possible length of a piece
    minFlexibleDistance = 1.0;
    
    // length of the pipe or other element to divide into pieces
    goal = 26.5;
    
    Change(new List<double>(), 0, 0, goal);
    
    // more than one way to "make change" may have been found
    // prefer using no varialbe length pieces and as few pieces as possible
    List<double> bestResult = new List<double>();
    
    if (results.Count == 0)
    {
        TaskDialog td = new TaskDialog("Error");
        td.MainInstruction = "Could not make pieces with total length = " + goal;
        td.MainContent = "Minimum flexible length = " + minFlexibleDistance;
        td.Show();
        return;
    }
    
    List<double> resultWithShortestFlex = results.OrderBy(q => q.Last()).FirstOrDefault();
    double shortestFlexPiece = resultWithShortestFlex.Last();
    
    double numberOfPieces = Double.PositiveInfinity;
    foreach (List<double> thisResult in results.Where(q => q.Last() == shortestFlexPiece))
    {
        if (thisResult.Count < numberOfPieces)
        {
            numberOfPieces = thisResult.Count;
            bestResult = thisResult;
        }            
    }
    
    Display(bestResult);
    
}

private static void Change(List<double> coins, double highest, double sum, double goal)
{
    if (sum == goal) // OK - perfect match with no flexible lengths
    {
        coins.Add(0); // 0 is the flexible length
        results.Add(coins);
        return;
    }

    if (sum > goal)
    {
        double floorValue = sum - highest;
        double remainder = goal - floorValue;
        if (remainder >= minFlexibleDistance) // OK - can make up difference with flexible piece
        {
            coins.RemoveAt(coins.Count - 1);
            coins.Add(remainder);
            results.Add(coins);
            return;
        }
        else // BAD - min flexible distance would be too small - do not use this set of values
        {
            return;
        }
    }

    foreach (double value in amounts)
    {
        if (value >= highest) // Only add higher or equal amounts.
        {
            List<double> copy = new List<double>(coins);
            copy.Add(value);
            Change(copy, value, sum + value, goal);
        }
    }
    return;
}

private static void Display(List<double> coins)
{
    string data = "";
    foreach (double amount in amounts)
    {
        double count = coins.Count(value => value == amount);
        string pieces = "pieces";
        if (count == 1)
            pieces = "piece";
        
        data += count + " " + pieces + " each " + amount + " feet long" + Environment.NewLine;
    }
    foreach (double coin in coins)
    {
        if (!amounts.Contains(coin))
        {
            data += "One variable length piece = " + coin + " feet long" + Environment.NewLine;
        }
    }
    TaskDialog td = new TaskDialog("Output");
    td.MainInstruction = "Best way to make " + goal.ToString() + " feet from pieces";
    td.MainContent = data;
    td.Show();
}

Want Autodesk to improve/fix the #Revit API? Submit an “Idea”

At the #RTCEUR “All you ever wanted to ask about the Revit API” panel, Sasha Crotty strongly recommended submitting your suggestions for making the API better to the Revit Ideas board. This is a place where other people can vote for requests and Autodesk managers and developers are paying attention to the most popular items.

Even if you have already made requests on Autodesk’s Revit API Forum, if you really want the change/fix to be made Autodesk would like to submit an “idea” too.

#RTCEUR Wish 2: Pipe Split and Hanger Creation

Brian wished for “splitting MEP Pipework at certain distances on the horizontals with Revit MEP Hangers”. Here’s a look at how the API can be used to create a new set of pipes of a specified length to replace one long pipe. Hangers are placed at the start and end of each new pipe.

One opportunity for improvement would be to do something more sophisticated at the end of the original pipe. If your pipe comes in standard lengths, you may not want to end up with a 1′ piece of pipe at the end if you need 37′ total feet of pipe and your standard length is 6′. If that sounds like an interesting problem to solve, send me a tweet and maybe I’ll post the solution tomorrow. But now its time to find out more about the PORT in Porto.

public void SplitPipe()
{
    UIDocument uidoc = this.ActiveUIDocument;
    Document doc = uidoc.Document;
    Pipe pipe = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element)) as Pipe;
    ElementId levelId = pipe.get_Parameter(BuiltInParameter.RBS_START_LEVEL_PARAM).AsElementId();

    Line pipeLine = ((LocationCurve)pipe.Location).Curve as Line;
    double segmentLength = 6;
    List<Line> newLines = new List<Line>();
    double i = 0;
    for (i=0; i <= pipeLine.Length - segmentLength; i = i + segmentLength)
    {
        XYZ end0 = pipeLine.Evaluate(i, false);
        XYZ end1 = pipeLine.Evaluate(i + segmentLength, false);
        newLines.Add(Line.CreateBound(end0, end1));
    }
    newLines.Add(Line.CreateBound(pipeLine.Evaluate(i, false), pipeLine.Evaluate(1, true)));
    
    FamilySymbol hangerSymbol = new FilteredElementCollector(doc)
        .OfClass(typeof(FamilySymbol))
        .Cast<FamilySymbol>()
        .FirstOrDefault(q => q.Family.Name == "Hanger" &&
                        q.Family.FamilyCategory.Id.IntegerValue == (int)BuiltInCategory.OST_PipeAccessory);
    
    using (Transaction t = new Transaction(doc, "Split Pipe"))
    {
        t.Start();
        View3D view3d = View3D.CreateIsometric(doc, new FilteredElementCollector(doc)
                                               .OfClass(typeof(ViewFamilyType))
                                               .Cast<ViewFamilyType>()
                                               .FirstOrDefault(q => q.ViewFamily == ViewFamily.ThreeDimensional).Id);
        
        List<Tuple<FamilyInstance, double>> hangers = new List<Tuple<FamilyInstance, double>>();
        for (int lineCtr = 0; lineCtr < newLines.Count; lineCtr++)
        {
            Pipe newPipe = Pipe.Create(doc, pipe.MEPSystem.GetTypeId(), pipe.PipeType.Id, levelId, newLines[lineCtr].GetEndPoint(0), newLines[lineCtr].GetEndPoint(1));
            ReferenceIntersector ri = new ReferenceIntersector(new ElementClassFilter(typeof(Ceiling)), FindReferenceTarget.Face, view3d);
            ReferenceWithContext rwc = ri.FindNearest(newLines[lineCtr].GetEndPoint(1), XYZ.BasisZ);
            if (rwc != null)
            {
                Reference r = rwc.GetReference();
                Ceiling ceiling = doc.GetElement(r) as Ceiling;
                Options opt = new Options();
                opt.ComputeReferences = true;
                PlanarFace ceilingBottomFace = null;
                Solid s = ceiling.get_Geometry(opt).Cast<GeometryObject>().FirstOrDefault(q => q is Solid) as Solid;
                foreach (Face face in s.Faces)
                {
                    if (face is PlanarFace)
                    {
                        PlanarFace pf = face as PlanarFace;
                        if (pf.FaceNormal.IsAlmostEqualTo(XYZ.BasisZ.Negate()))
                        {
                            
                            ceilingBottomFace = pf;
                            break;
                        }
                    }
                }
                if (ceilingBottomFace != null)
                {
                    FamilyInstance fiHanger = doc.Create.NewFamilyInstance(ceilingBottomFace, newLines[lineCtr].GetEndPoint(1), pipeLine.Direction.CrossProduct(XYZ.BasisZ), hangerSymbol);
                    hangers.Add(new Tuple<FamilyInstance, double>(fiHanger, rwc.Proximity));
                    if (lineCtr == 0)
                    {
                        fiHanger = doc.Create.NewFamilyInstance(ceilingBottomFace, newLines[lineCtr].GetEndPoint(0), pipeLine.Direction.CrossProduct(XYZ.BasisZ), hangerSymbol);    
                        hangers.Add(new Tuple<FamilyInstance, double>(fiHanger, rwc.Proximity));
                    }
                }
            }
        }
        
        foreach (Tuple<FamilyInstance, double> tup in hangers)
        {
            tup.Item1.Parameters.Cast<Parameter>().FirstOrDefault(q => q.Definition.Name == "Distance from Ceiling to Pipe Center").Set(tup.Item2);
        }
        
        doc.Delete(pipe.Id);
        doc.Delete(view3d.Id);
        t.Commit();
    }
}

#RTCEUR API Wish #1: Purging Types and Families

The first wish granted from Porto is for Phil who wished for “a solution to purging unused Revit elements from a Revit model”. Different elements need to be purged in different ways. This code will clean up unused system and loadable families and their types.

How are you wishing to make Revit better?

purge-browser

The “RollbackIfErrorOccurs” section is used to handle the case where deleting a wall type would result in this error.

last-type-in-system-family

public void purgeFamiliesAndTypes()
{
    Document doc = this.ActiveUIDocument.Document;
    
    // List of categories whose families will be purged
    List<int> categoriesToPurge = new List<int>();
    categoriesToPurge.Add((int)BuiltInCategory.OST_StructuralFraming);
    categoriesToPurge.Add((int)BuiltInCategory.OST_Walls);

    List<ElementId> typesToDelete = new List<ElementId>();

    // Check all element types whose category is contained in the category list	
    foreach (ElementType et in new FilteredElementCollector(doc)
             .OfClass(typeof(ElementType))
             .Cast<ElementType>()
             .Where(q => q.Category != null && 
                    categoriesToPurge.Contains(q.Category.Id.IntegerValue)))
    {
        // if there are no elements with this type, add it to the list for deletion
        if (new FilteredElementCollector(doc)
            .WhereElementIsNotElementType()
            .Where(q => q.GetTypeId() == et.Id).Count() == 0)
        {
            typesToDelete.Add(et.Id);
        }
    }
        
    using (TransactionGroup tg = new TransactionGroup(doc, "Purge families"))
    {
        tg.Start();
        foreach (ElementId id in typesToDelete)
        {    
            using (Transaction t = new Transaction(doc, "delete type"))
            {
                // Do not delete type if it would result in error such as
                // "Last type in system family "Stacked Wall" cannot be deleted."
                FailureHandlingOptions failOpt = t.GetFailureHandlingOptions();
                failOpt.SetClearAfterRollback(true);
                failOpt.SetFailuresPreprocessor(new RollbackIfErrorOccurs());
                t.SetFailureHandlingOptions(failOpt);
                
                t.Start();
                try
                {
                    doc.Delete(id);
                }
                catch
                {}
                t.Commit();
            }    
        }
        
        // Delete families that now have no types
        IList<ElementId> familiesToDelete = new List<ElementId>();    
        foreach (Family family in new FilteredElementCollector(doc)
            .OfClass(typeof(Family))
            .Cast<Family>()
            .Where(q => categoriesToPurge.Contains(q.FamilyCategory.Id.IntegerValue)))
        {
            // add family to list if there are no instances of any type of this family
            if (new FilteredElementCollector(doc)
                .OfClass(typeof(FamilyInstance))
                .Cast<FamilyInstance>()
                .Where(q => q.Symbol.Family.Id == family.Id)
                .Count() == 0)
            {
                familiesToDelete.Add(family.Id);
            }
        }
        
        using (Transaction t = new Transaction(doc, "delete families with no types"))
        {
            t.Start();
            doc.Delete(familiesToDelete);
            t.Commit();
        }
        
        tg.Assimilate();
    }
}

public class RollbackIfErrorOccurs : IFailuresPreprocessor
{
    public FailureProcessingResult PreprocessFailures(FailuresAccessor failuresAccessor)
    {
        // if there are any failures, rollback the transaction
        if (failuresAccessor.GetFailureMessages().Count > 0)
        {
            return FailureProcessingResult.ProceedWithRollBack;
        }
        else
        {
            return FailureProcessingResult.Continue;
        }
    }
}        

Get your #Revit API #RTCEUR Wishes Ready

Another Revit Technology Conference is almost here… That means it is almost time for Boost Your BIM to grant another round of Revit API Wishes!

Start thinking about now about how you’d like to Make Revit Better and post them as comments here or tweet them to @BoostYourBIM. Wishes will be granted live from Porto starting on Thursday morning right up until Saturday’s gala dinner.

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