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.