Opening a local file’s central file – Attempt #2

public void OpenCentralFile()
{
    Autodesk.Revit.ApplicationServices.Application app = this.Application;
    Document doc = this.ActiveUIDocument.Document;

    // Get an instance of the ModelPath class describing the central file
    ModelPath modelPath = doc.GetWorksharingCentralModelPath();

    // Get the string of the path for the modelPath 
    string centralFilepath = ModelPathUtils.ConvertModelPathToUserVisiblePath(modelPath);

    // Local and central can't both be open at the same time in the same session
    // But trying to call doc.Close() here doesn't work. It throws an InvalidOperationException
    // when attempting to close the currently active document
    doc.Close();

    Document centralDoc = app.OpenDocumentFile(centralFilepath);
    TaskDialog.Show("status", "Central file opened");
}

How to Not Open a Local File’s Central File

The last post about errors when synchronizing with the central file got me thinking about how the API might be used to run checks in the central file before saving.

At first, it seems like it should be simple to open the local file’s central file. Maybe it should be, but it isn’t.

Here is a straightforward first attempt. The user opens the local file. Then this macro runs to open and investigate the central file.

public void OpenCentralFile()
{
    Document doc = this.ActiveUIDocument.Document;

    // Get an instance of the ModelPath class describing the central file
    ModelPath modelPath = doc.GetWorksharingCentralModelPath();

    // Get the string of the path for the modelPath 
    string centralFilepath = ModelPathUtils.ConvertModelPathToUserVisiblePath(modelPath);

    Autodesk.Revit.ApplicationServices.Application app = this.Application;
    // Open the Central File
    Document centralDoc = app.OpenDocumentFile(centralFilepath);

    // We never get here because the local and central can't both be open at the same time in the same session
    TaskDialog.Show("status", "Central file opened");
}

error

Revit users may have seen this restriction in the past, and with the API the restriction is no different. The next post will look at how to workaround this limitation.

Capture Comments When Saving to Central

Steve at Revit OpEd noted recently that occasionally he encounters a bigger problem (room warnings during Save To Central) and a smaller problem (comments entered in the Synchronize With Central dialog are lost):

If you are like me you enter a comment each time (using SwC) for tracking and troubleshooting purposes. It is a pain to get a message telling you your SwC didn’t work. It’s worse that what you typed is lost. I finally got slightly smarter. I try to remember to copy the comment text to the clipboard before clicking OK. This way I can just paste it back in if/or when the error pops up.

We can’t use the API to fix the bigger problem (though there may be some API solution to help mitigate it). But we can definitely use the API to make sure that the comments aren’t lost.

If we subscribe to the DocumentSynchronizingWithCentralEvent, we can use the DocumentSynchronizingWithCentralEventArgs.Comments property to get the comments entered in the Synchronize With Central dialog box. Then we can write them to a text file, database, or other location where they will be safe, regardless of Revit’s success or failure to complete the save.

See my previous post on Events for an introduction to the functionality.

public void SynchWithCentralEventRegister()
{
    this.Application.DocumentSynchronizingWithCentral += new EventHandler<DocumentSynchronizingWithCentralEventArgs>(mySynchronizingWithCentralEvent);
}        

private void mySynchronizingWithCentralEvent(object sender, DocumentSynchronizingWithCentralEventArgs args)
{
    string comments = args.Comments;
    
        // Use the @ before the filename to avoid errors related to the \ character
    // Alternative is to use \\ instead of \
    string filename = @"C:\Users\HP002\Documents\SynchronizingWithCentral Comments.txt";

    // Create a StreamWriter object to write to a file
    using (System.IO.StreamWriter writer = new System.IO.StreamWriter(filename))
    {
        writer.WriteLine(DateTime.Now + " - " + comments);
    }    
    // open the text file
    System.Diagnostics.Process.Start(filename);
}

How to create a door & tag

Continuing the theme of element creation from the last post, here is how to create a door and tag it.

public void CreateDoor()
{
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = this.ActiveUIDocument;

    // Get the door type
    // FamilySymbol corresponds to the Type of a Family
    // In this case Door - Single-Flush - 34" x 80"
    FamilySymbol familySymbol = (from fs in new FilteredElementCollector(doc).
         OfClass(typeof(FamilySymbol)).
         Cast<FamilySymbol>()
         // need to put the \ before the " in the name of the door type so it isn't
         // treaded as the " that ends the string
         where (fs.Family.Name == "Single-Flush" && fs.Name == "34\" x 80\"")
         select fs).First();

    // Get the door tag
    FamilySymbol doorTagType = (from tag in new FilteredElementCollector(doc)
                                .OfClass(typeof(FamilySymbol)).OfCategory(BuiltInCategory.OST_DoorTags)
                                .Cast<FamilySymbol>() where tag.Name == "Door Tag" select tag).First();

    // Prompt the user to select a wall
    Reference r = uidoc.Selection.PickObject(ObjectType.Element, "Select the wall that will host the door");
    // Get the wall element from the selected reference
    Wall wall = doc.GetElement(r) as Wall;
    // Get the point on the wall where the selection was made
    XYZ globalPoint = r.GlobalPoint;
    // But we don't want to place the door at this "global point", because the Z of that point is at the
    // cut plane height of the plan view.

    // Instead, create a new XYZ using the X and Y from the GlobalPoint and the Z of the plan view's level
    ViewPlan viewPlan = doc.ActiveView as ViewPlan;
    Level level = viewPlan.GenLevel;
    XYZ levelPoint = new XYZ(globalPoint.X, globalPoint.Y, level.Elevation);

    using (Transaction t = new Transaction(doc,"Create door"))
    {
        t.Start();
        // Create door
        FamilyInstance door = doc.Create.NewFamilyInstance(levelPoint, familySymbol, e, Autodesk.Revit.DB.Structure.StructuralType.NonStructural);
        // Create tag.
        // Unlike room tags, which have a dedicated NewRoomTag method, the generic NewFamilyInstance is used to tag doors and other elements.
        FamilyInstance doorTag = doc.Create.NewFamilyInstance(levelPoint, doorTagType, doc.ActiveView);
        t.Commit();
    }
}

How to create a room and tag it

An AUGI user asked

“Is there a way to get the UV point from a user clicking the floor plan view during an active API session?

My goal is provide the user with a list of rooms to place and have them select a room from the list in form, click a point on the floor plan view and create the room via the API.”

Here is a macro to create a room with a room tag

public void CreateRoom()
{
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = this.ActiveUIDocument;

    // Get the level of the plan view
    ViewPlan view = doc.ActiveView as ViewPlan;
    Level level = view.GenLevel; // use GenLevel, not Level

    // Create a UV from a point selected by the user (the Z value is not needed)
    XYZ xyz = uidoc.Selection.PickPoint("Select a point");
    UV uv = new UV(xyz.X, xyz.Y);

    using (Transaction t = new Transaction(doc, "Create Room"))
    {
        t.Start();
        Room room = doc.Create.NewRoom(level, uv);
        RoomTag tag = doc.Create.NewRoomTag(room,uv,view);
        t.Commit();
    }
}

Converting a Macro into an Add-In

Here is a summary of how to convert this macro into an add-in command.

public void MyMacro()
{
    Document doc = this.ActiveUIDocument.Document;
    ICollection<Element> wallList = new FilteredElementCollector(doc).OfClass(typeof(Wall)).ToElements();
    TaskDialog.Show("Wall Count", wallList.Count.ToString());
}

1) Create a new “Class Library” project in the development environment of your choice (I am using the free Microsoft Visual Studio Express 2012 for Windows Desktop for this example)

NewProject

2) Right click on the References folder and select “Add Reference”

addReferences

3) This is the code that goes into the “Class1.cs” file that Visual Studio creates

using System;
using System.Collections.Generic;
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using Autodesk.Revit.Attributes;
namespace MyAddinTest
{
    // Specifies that the command will be responsible for creating any needed transactions
    [Transaction(TransactionMode.Manual)]
    // "MyAddInCommand" is the name of the command
    public class MyAddInCommand : IExternalCommand
    {
        // every command must have a unique GUID (globally unique identifier) of any 32 hexadecimal digits
        // you can modify an GUID you are using in some other macro or create one in Visual Studio with Tools - Create GUID
        // or http://www.guidgenerator.com/online-guid-generator.aspx
        static AddInId appId = new AddInId(new Guid("21A8C920-ED49-434A-AACD-176784316B92"));
        public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elementSet)
        {
            // the code get the document is changed from the Macro version
            Document doc = commandData.Application.ActiveUIDocument.Document;
            ICollection<Element> wallList = new FilteredElementCollector(doc).OfClass(typeof(Wall)).ToElements();
            TaskDialog.Show("Wall Count", wallList.Count.ToString());

            // Unlike a macro, which does not return any value, an add-in needs to return a "result" value
            // specifying if the command succeeded, failed, or was cancelled.
            return Result.Succeeded;
        }
    }
}

4) Compile it with the command “Build – Build Solution”. This creates a file MyAddinTest.dll. On my machine it is in the folder C:\Users\HP002\Documents\Visual Studio 2012\Projects\MyAddinTest\MyAddinTest\bin\Debug

5) Create an .addin file. Mine is in C:\Users\HP002\AppData\Roaming\Autodesk\revit\Addins\2013. The .addin file can have any name that you want. Here is the contents of my BoostYourBIM.addin file.

<?xml version="1.0" encoding="utf-8"?>
<RevitAddIns>
   <AddIn Type="Command">
    <Assembly>C:\Users\HP002\Documents\Visual Studio 2012\Projects\MyAddinTest\MyAddinTest\bin\Debug\MyAddinTest.dll</Assembly>
    <AddInId>21A8C920-ED49-434A-AACD-176784316B92</AddInId>
    <FullClassName>MyAddinTest.MyAddInCommand</FullClassName>
	<Text>Count Walls</Text>
	<Description>Count the number of walls in this project</Description>
    <VendorId>test</VendorId>
  </AddIn> 
 </RevitAddIns>

6) Run Revit. The External Tools menu on the Add-Ins”Count Walls” external command will include the Count Walls command.

addin

Commenting and Uncommenting in SharpDevelop

The SharpDevelop UI has a “Comment Region” command that you can use after selecting multiple lines of code.

In Microsoft Visual Studio, there are 2 different commands – Comment Region and Uncomment Region

In SharpDevelop there is no Uncomment Region command, but you can use Comment Region on code that is already commented and it will remove the comment notation.

CommentRegion

Using Statements

Here is the set of “using” statements that I currently have in my macros file

using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Collections.Generic;
using Autodesk.Revit.DB;
using Autodesk.Revit.DB.Architecture;
using Autodesk.Revit.DB.Events;
using Autodesk.Revit.UI.Events;
using Autodesk.Revit.UI;
using Autodesk.Revit.UI.Selection;
using Autodesk.Revit.ApplicationServices;

For the output to Excel post I also used

using Microsoft.Office.Interop.Excel;

But as I noted in that post, that namespace and Autodesk.Revit.DB both have classes named Line and Parameter, so to avoid that I have commented out the Excel using statement and the macro that does the Excel output.

If you are missing “using” statements, the error you will see is like this one:

The type or namespace name ‘Room’ could not be found (are you missing a using directive or an assembly reference?) (CS0246) –

Using 3D Section Box to set Plan View’s View Range

I was asked if it was possible to set the view range of a floor plan to match the top and bottom extents of a 3d section box. Yes, it is, and here is a little video showing it in action.

  • The view on the left is the 3D view with Section Box
  • The view on the right is the plan view
  • Change the top or bottom of the section box
  • Switch to the plan view
  • Run the macro and the view range is updated
public void SetViewRange()
{
    Document doc = this.ActiveUIDocument.Document;

    // Get the active view and cast it to a ViewPlan (the API's class for Plan View)
    ViewPlan viewPlan = doc.ActiveView as ViewPlan;

    // Error handling in case the active view is not a plan view
    if (viewPlan == null)
    {
        TaskDialog.Show("Error", "Active view must be a plan view.");
        // return ends the execution of the macro
        return;
    }

    // Get the level that generates the plan view
    Level level = viewPlan.GenLevel;

    View3D view3d = null;
    try
    {
        // if there is no 3D view whose name equals viewPlan.Name, then First() will throw an exception
        view3d = (from v in new FilteredElementCollector(doc).OfClass(typeof(View3D)).Cast<View3D>() where v.Name == viewPlan.Name select v).First();
    }
    catch
    {
        TaskDialog.Show("Error", "There is no 3D view named '" + viewPlan.Name + "'");
        return;
    }

    // Get the BoundingBoxXYZ (a 3D rectangular box) that describes the shape of the Section Box
    BoundingBoxXYZ bbox = view3d.SectionBox;

    // The coordinates of the bounding box are defined relative to a coordinate system specific to the bounding box
    // When setting the view range offsets, the values will need to be relative to the model 

    // This transform translates from coordinate system of the bounding box to the model coordinate system
    Transform transform = bbox.Transform;
    // Transform.Origin defines the origin of the bounding box's coordinate system in the model coordinate system
    // The Z value indicates the vertical offset of the bounding box's coordinate system
    double bboxOriginZ = transform.Origin.Z;

    // BoundingBoxXYZ.Min.Z and BoundingBoxXYZ.Max.Z give the Z values for the bottom and top of the section box
    // Adding the Transform.Origin.Z converts the value to the model coordinate system
    double minZ = bbox.Min.Z + bboxOriginZ;
    double maxZ = bbox.Max.Z + bboxOriginZ;

    // Get the PlanViewRange object from the plan view
    PlanViewRange viewRange = viewPlan.GetViewRange();

    // Set all planes of the view range to use the plan view's level
    viewRange.SetLevelId(PlanViewPlane.TopClipPlane, level.Id);
    viewRange.SetLevelId(PlanViewPlane.CutPlane, level.Id);
    viewRange.SetLevelId(PlanViewPlane.BottomClipPlane, level.Id);
    viewRange.SetLevelId(PlanViewPlane.ViewDepthPlane, level.Id);

    // Set the view depth offset to the difference between the bottom of the section box and
    // the elevation of the level
    viewRange.SetOffset(PlanViewPlane.ViewDepthPlane, minZ - level.Elevation);

    // Set all other offsets to to the difference between the top of the section box and
    // the elevation of the level
    viewRange.SetOffset(PlanViewPlane.TopClipPlane, maxZ - level.Elevation);
    viewRange.SetOffset(PlanViewPlane.CutPlane, maxZ - level.Elevation);
    viewRange.SetOffset(PlanViewPlane.BottomClipPlane, maxZ - level.Elevation);

    using (Transaction t = new Transaction(doc, "Set View Range"))
    {
        t.Start();
        // Set the view range
        viewPlan.SetViewRange(viewRange);
        t.Commit();
    }
}

Divide Parts at Levels

On the AUGI Revit API wish list it was mentioned that it would be nice to be able to divide parts at levels.

Wish granted!

divideparts


public void CreatePartAndDivideAtLevels()
{
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = new UIDocument(doc);
    ElementId id = uidoc.Selection.PickObject(ObjectType.Element).ElementId;
    IList<ElementId> ids = new List<ElementId>();
    ids.Add(id);

    using (Transaction t = new Transaction(doc,"Create Part"))
    {
        t.Start();
        // Create parts from the selected element
        // There is "CreateParts" but no "CreatePart", so needed to use a list containing the one element
        PartUtils.CreateParts(doc,ids);
        t.Commit();
    }

    // Get the newly created parts
    ICollection<ElementId> partsList = PartUtils.GetAssociatedParts(doc,id,false,false);

    // Get all levels
    ICollection<ElementId> levels = new FilteredElementCollector(doc).OfClass(typeof(Level)).OfCategory(BuiltInCategory.OST_Levels).ToElementIds();

    // Create a list of curves which needs to be used in DivideParts but for this example
    // the divide is being done by levels so the curve list will be empty
    IList<Curve> curveList = new List<Curve>();

    // Get the host object corresponding to the selected element
    // HostObject is the parent class for walls, roof, floors, etc.
    HostObject hostObj = doc.GetElement(id) as HostObject;

    // Get the reference of one of the major faces of the selected element
    // Will be used to create a sketch plane
    Reference r = HostObjectUtils.GetSideFaces(hostObj, ShellLayerType.Exterior).First();

    using (Transaction t = new Transaction(doc,"Divide Part at Levels"))
    {
        t.Start();
        SketchPlane sketchPlane = doc.Create.NewSketchPlane(r);
        // Divide the parts
        PartUtils.DivideParts(doc, partsList, levels, curveList, sketchPlane.Id);
        t.Commit();
    }

    // Set the view's "Parts Visibility" parameter so that parts are shown
    Parameter p = doc.ActiveView.get_Parameter(BuiltInParameter.VIEW_PARTS_VISIBILITY);
    using (Transaction t = new Transaction(doc,"Set View Parameter"))
    {
        t.Start();
        p.Set(0); // 0 = Show Parts, 1 = Show Original, 2 = Show Both
        t.Commit();
    }            
}

Measuring Glass Area In a Door Above 2′

Following up on the previous post, here is how to put all the pieces together to find a door’s glass area above 2′

  1. Find the glass area of the door as-is
  2. Edit the door family
  3. Create a void to eliminate the bottom 2′ of glass
  4. Reload the door into the project
  5. Find the glass area of the modified door
  6. Rollback the changes to the door and the reloading of the door into the RVT

glassarea

public void MesaureGlassArea()
{
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = new UIDocument(doc);
    FamilyInstance instance = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element)) as FamilyInstance;

    // Get the surface area of glass in the family
    string info = "Total glass area in door = " + getGlassAreaInFamily(instance) + "\n";

    // Open and edit the family document
    Document familyDoc = doc.EditFamily(instance.Symbol.Family);

    // The family geometry is going to be edited (to create the void to cut off the bottom 2' of glass) and then reloaded into the RVT.
    // After this is done, call getGlassAreaInFamily to get the modified glass area.
    // But in the end we don't want to keep all the transactions created by CreateExtrusionAndCutGlass.
    // A transaction group can be used to rollback all the transactions that were committed inside the transaction group.
    using (TransactionGroup transactionGroup = new TransactionGroup(doc,"Glass measurement"))
    {
        transactionGroup.Start();

        // Cut off the bottom 2 feet of glass
        CreateExtrusionAndCutGlass(familyDoc);

        // Load the modified family back into the RVT
        // FamilyLoadOptions tells Revit to overwrite the existing family instances in the RVT
        FamilyLoadOptions loadOptions = new FamilyLoadOptions();
        familyDoc.LoadFamily(doc,loadOptions);

        // Get the surface area of glass in the modified family
        info += "Glass in door above 2 feet = " + getGlassAreaInFamily(instance);    

        // Rollback the transaction group so that the reloading of the family and modifications to the family are discarded.
        transactionGroup.RollBack();
    }

    TaskDialog.Show("Material info", info);

    // Close the family document. False means do not save the file
    familyDoc.Close(false);
}

private double getGlassAreaInFamily(Element element)
{
    foreach (Material material in element.Materials)
    {
        if (material.Name == "Glass")
            return element.GetMaterialArea(material);
    }    
    return 0;
}

// FamilyLoadOptions tells Revit what to do when loading the family into a document that already contains the family
public class FamilyLoadOptions : IFamilyLoadOptions
{
    // Always return true so that all existing families and their parameters are overwritten with the new family
    public bool OnFamilyFound(bool familyInUse, out bool overwriteParameterValues)
    {
        overwriteParameterValues = true;
        return true;
    }
    public bool OnSharedFamilyFound(Family sharedFamily, bool familyInUse, out FamilySource source, out bool overwriteParameterValues)
    {
        overwriteParameterValues = true;
        source = FamilySource.Family;
        return true;
    }
}

private void CreateExtrusionAndCutGlass(Document doc)
{
    Autodesk.Revit.ApplicationServices.Application app = doc.Application;

    // Height of the void extrusion
    double height = 2;

    // Four points to define corners of the rectangle to extrude
    XYZ pnt1 = new XYZ(-10, -10, 0);
    XYZ pnt2 = new XYZ(10, -10, 0);
    XYZ pnt3 = new XYZ(10, 10, 0);
    XYZ pnt4 = new XYZ(-10, 10, 0);

    // Create the four lines of the rectangle
    // These are internal "Line" elements, not model lines or detail lines
    Line line1 = app.Create.NewLine(pnt1, pnt2, true);
    Line line2 = app.Create.NewLine(pnt2, pnt3, true);
    Line line3 = app.Create.NewLine(pnt3, pnt4, true);
    Line line4 = app.Create.NewLine(pnt4, pnt1, true);

    // Put these lines into a CurveArray
    CurveArray curveArray = new CurveArray();
    curveArray.Append(line1);
    curveArray.Append(line2);
    curveArray.Append(line3);
    curveArray.Append(line4);

    // Put this array into a CureArrArray (an array of CurveArrays)
    // Extrusion creation uses a CureArrArray so you can extrusion multiple loops 
    CurveArrArray curveArrayArray = new CurveArrArray();
    curveArrayArray.Append(curveArray);

    // Create a plane at the origin with a normal in the up direction
    XYZ planeNormal = XYZ.BasisZ;
    XYZ planeOrigin = XYZ.Zero;
    Plane plane = app.Create.NewPlane(planeNormal, planeOrigin);

    Extrusion extrusion = null;

    using (Transaction t = new Transaction(doc, "Create Extrusion"))
       {
            t.Start();
            // Create a sketch plane to be used for the extrusion
            SketchPlane sketchPlane = doc.FamilyCreate.NewSketchPlane(plane);
            // Create the extrusion. The "false" specifies that it will be a void
            extrusion = doc.FamilyCreate.NewExtrusion(false, curveArrayArray, sketchPlane, height);
            t.Commit();
       }

    // Cut the other 3D elements with the new void using CombineElements and a CombinableElementArray
    CombinableElementArray ceArray = new CombinableElementArray();

    // Add the new void extrusion to the CombinableElementArray
    ceArray.Append(extrusion);

    // Add all GenericForm elements with the Glass subcategory to a CombinableElementArray
    foreach (GenericForm genericForm in new FilteredElementCollector(doc).OfClass(typeof(GenericForm)).Cast<GenericForm>())
    {
        Category category = genericForm.Subcategory;
        if (category != null && category.Name == "Glass")
        {
            ceArray.Append(genericForm);
        }
    }

    using (Transaction t = new Transaction(doc, "Combine Elements"))
       {
            t.Start();
            // Combine the elements so that the void will cut the solids
            GeomCombination geomCombination = doc.CombineElements(ceArray);
            t.Commit();
       }
}

Creating a Void to Cut Family Geometry

In a comment to “Calculating the glass in a door“, the question was posed:

“Let’s say I have a 7′ tall, full glass door. How complicated would it be to add constraints, or limits, to the area in which you want to calculate? Say I want to calculate the total are of glass between 2′ and 6′ (assuming the total glass area was greater than that).”

The place to start answering this question is with API code to create a void that eliminates the glass in the door below 2′. The image below was created with “Double-Glass 1.rfa”.

cutGlass

public void CreateExtrusionAndCutGlass()
{
    Document doc = this.ActiveUIDocument.Document;
    Autodesk.Revit.ApplicationServices.Application app = doc.Application;

    // Height of the void extrusion
    double height = 2;

    // Four points to define corners of the rectangle to extrude
    XYZ pnt1 = new XYZ(-10, -10, 0);
    XYZ pnt2 = new XYZ(10, -10, 0);
    XYZ pnt3 = new XYZ(10, 10, 0);
    XYZ pnt4 = new XYZ(-10, 10, 0);

    // Create the four lines of the rectangle
    // These are internal "Line" elements, not model lines or detail lines
    Line line1 = app.Create.NewLine(pnt1, pnt2, true);
    Line line2 = app.Create.NewLine(pnt2, pnt3, true);
    Line line3 = app.Create.NewLine(pnt3, pnt4, true);
    Line line4 = app.Create.NewLine(pnt4, pnt1, true);

    // Put these lines into a CurveArray
    CurveArray curveArray = new CurveArray();
    curveArray.Append(line1);
    curveArray.Append(line2);
    curveArray.Append(line3);
    curveArray.Append(line4);

    // Put this array into a CureArrArray (an array of CurveArrays)
    // Extrusion creation uses a CureArrArray so you can extrusion multiple loops 
    CurveArrArray curveArrayArray = new CurveArrArray();
    curveArrayArray.Append(curveArray);

    // Create a plane at the origin with a normal in the up direction
    XYZ planeNormal = XYZ.BasisZ;
    XYZ planeOrigin = XYZ.Zero;
    Plane plane = app.Create.NewPlane(planeNormal, planeOrigin);

    Extrusion extrusion = null;

    using (Transaction t = new Transaction(doc, "Create Extrusion"))
       {
            t.Start();
            // Create a sketch plane to be used for the extrusion
            SketchPlane sketchPlane = doc.FamilyCreate.NewSketchPlane(plane);
            // Create the extrusion. The "false" specifies that it will be a void
            extrusion = doc.FamilyCreate.NewExtrusion(false, curveArrayArray, sketchPlane, height);
            t.Commit();
       }

    // Cut the other 3D elements with the new void using CombineElements and a CombinableElementArray
    CombinableElementArray ceArray = new CombinableElementArray();

    // Add the new void extrusion to the CombinableElementArray
    ceArray.Append(extrusion);

    // Add all GenericForm elements with the Glass subcategory to a CombinableElementArray
    foreach (GenericForm genericForm in new FilteredElementCollector(doc).OfClass(typeof(GenericForm)).Cast<GenericForm>())
    {
        Category category = genericForm.Subcategory;
        if (category != null && category.Name == "Glass")
        {
            ceArray.Append(genericForm);
        }
    }

    using (Transaction t = new Transaction(doc, "Combine Elements"))
       {
            t.Start();
            // Combine the elements so that the void will cut the solids
            GeomCombination geomCombination = doc.CombineElements(ceArray);
            t.Commit();
       }
}

Create a 3D View with Section Box for each Level

In a comment on the Payette blog, it was asked how to make a 3D view showing a single floor of a building. This is done with the Section Box, and I thought it would make a good API sample to make a 3D view with Section Box for every level of a project.

Here are the set of 3D views that the macro creates, one for each of the 5 levels in the project.

3dSectionBox

To start, here is a small macro to create a 3d view.  It introduces the new concept of ViewFamilyType. If you click Edit Type when the Properties of a view are shown, the Type that is shown in the ViewFamilyType. Many Revit users probably ever have to deal with this Type, but to create a view with the API it needs to be specified.

ViewFamilyType

public void create3DView()
{
    Document doc = this.ActiveUIDocument.Document;

    // get a ViewFamilyType for a 3D View
    ViewFamilyType viewFamilyType = (from v in new FilteredElementCollector(doc).
                 OfClass(typeof(ViewFamilyType)).
                 Cast<ViewFamilyType>()
                 where v.ViewFamily == ViewFamily.ThreeDimensional
                 select v).First();

    using (Transaction t = new Transaction(doc,"Create view"))
    {    
        t.Start();
        View3D view = View3D.CreateIsometric(doc, viewFamilyType.Id);
        t.Commit();
    }
}

Building on that, here is the macro to create the set of views shown above and set the Section Boxes based on the height of each level.

public void create3DViewsWithSectionBox()
{
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = new UIDocument(doc);

    // get list of all levels
    IList<Level> levels = new FilteredElementCollector(doc).OfClass(typeof(Level)).Cast<Level>().OrderBy(l => l.Elevation).ToList();

    // get a ViewFamilyType for a 3D View
    ViewFamilyType viewFamilyType = (from v in new FilteredElementCollector(doc).
                                     OfClass(typeof(ViewFamilyType)).
                                     Cast<ViewFamilyType>()
                                     where v.ViewFamily == ViewFamily.ThreeDimensional
                                     select v).First();

    using (Transaction t = new Transaction(doc,"Create view"))
    {
        int ctr = 0;
        // loop through all levels
        foreach (Level level in levels)
        {
            t.Start(); 

            // Create the 3d view
            View3D view = View3D.CreateIsometric(doc, viewFamilyType.Id);

            // Set the name of the view
            view.Name = level.Name + " Section Box";

            // Set the name of the transaction
            // A transaction can be renamed after it has been started
            t.SetName("Create view " + view.Name);

            // Create a new BoundingBoxXYZ to define a 3D rectangular space
            BoundingBoxXYZ boundingBoxXYZ = new BoundingBoxXYZ();

            // Set the lower left bottom corner of the box
            // Use the Z of the current level.
            // X & Y values have been hardcoded based on this RVT geometry
            boundingBoxXYZ.Min = new XYZ(-50, -100, level.Elevation);

            // Determine the height of the bounding box
            double zOffset = 0;
            // If there is another level above this one, use the elevation of that level
            if (levels.Count > ctr + 1)
                zOffset = levels.ElementAt(ctr+1).Elevation;
            // If this is the top level, use an offset of 10 feet
            else
                zOffset = level.Elevation + 10;
            boundingBoxXYZ.Max = new XYZ(200, 125, zOffset);

            // Apply this bouding box to the view's section box
            view.SectionBox = boundingBoxXYZ;

            t.Commit();

            // Open the just-created view
            // There cannot be an open transaction when the active view is set
            uidoc.ActiveView = view;

            ctr++;            
        }
    }
}

UPDATE: The list of levels is now sorted by Elevation with the addition of “OrderBy(l => l.Elevation)”

Automatically Run API Code When Your Model Changes (Does this toilet make my wall look thin?)

Over at Revit SWAT there is a great post about how to modify a wall-hosted family so it gives an alert when the host wall is too thin. This topic is a great excuse for me to write about Dynamic Model Update, the Revit API feature that automatically runs API code when your Revit model changes.

The idea in this example is to alert the user when a toilet is placed on a wall that is too thin to hold its plumbing. To do this, we need to have API code that runs automatically when the model changes.

But before coding such a real-time alert, here is a quick sample showing how to check all plumbing fixtures that are already in the model.

public void checkPlumbingWallWidth()
{
    Document doc = this.ActiveUIDocument.Document;
    string tooThin = "";
    SelElementSet selSet = SelElementSet.Create();
    foreach (FamilyInstance inst in (from i in new FilteredElementCollector(doc)
             .OfClass(typeof(FamilyInstance)) // find only FamilyInstances
             .OfCategory(BuiltInCategory.OST_PlumbingFixtures) // find only Plumbing Fixtures
             .Cast<FamilyInstance>() // cast to FamilyInstance so FamilyInstance.Host can be used
             where ((Wall)i.Host).Width < 0.5 // FamilyInstance.Host returns an Element, so cast it to a Wall so Wall.Width can be used
             select i))
    {
        tooThin += inst.Symbol.Family.Name + ", id = " + inst.Id + "\n";
        selSet.Add(inst); // add the instance to the selection set
    }
    UIDocument uidoc = new UIDocument(doc);
    uidoc.Selection.Elements = selSet; 
    uidoc.RefreshActiveView();
    TaskDialog.Show("Plumbing in Walls < 6\"",tooThin);
}

fixtures

For more info on the SelElementSet functionality to select the elements that are found, see https://boostyourbim.wordpress.com/2012/12/15/retrieving-and-selecting-elements-in-a-selectionfilterelement/


Now for the real-time alert!

With the code below, placing the toilet on the vertical (thicker) wall gives no warning. But placing one on the thin horizontal wall gives the warning shown.

wallwarning

Below is the code for 3 macros:

  • FamilyInstanceUpdater – the code that runs when the trigger occurs
  • RegisterUpdater to turn on the updater
  • UnregisterUpdater to turn off the updater
public class FamilyInstanceUpdater : IUpdater
{
    static AddInId m_appId;
    static UpdaterId m_updaterId;
    // constructor takes the AddInId for the add-in associated with this updater
    public FamilyInstanceUpdater(AddInId id)
    {
        m_appId = id;
        // every Updater must have a unique ID
        m_updaterId = new UpdaterId(m_appId, new Guid("FBFBF6B2-4C06-42d4-97C1-D1B4EB593EFF"));
    }
    public void Execute(UpdaterData data)
    {
        Document doc = data.GetDocument();

        // loop through the list of added elements
        foreach (ElementId addedElemId in data.GetAddedElementIds())
        {
            // check if the added element is a family instance of the Plumbing Fixtures category
            FamilyInstance instance = doc.GetElement(addedElemId) as FamilyInstance;
            if (instance != null && instance.Category.Name == "Plumbing Fixtures")
            {
                // Get the instance's host wall
                Wall wall = instance.Host as Wall;
                // Check that there is a host wall and that its width is greater than 6"
                // Revit API uses feet for distance measurements
                if (wall != null && wall.Width < 0.5)
                    TaskDialog.Show("Warning!", "Your wall is too thin!");
            }
        }
    }
    public string GetAdditionalInformation(){return "Family Instance host wall thickness check";}
    public ChangePriority GetChangePriority(){return ChangePriority.FloorsRoofsStructuralWalls;}
    public UpdaterId GetUpdaterId(){return m_updaterId;}
    public string GetUpdaterName(){return "Wall Thickness Check";}
}

// This command must be run to register the updater with Revit
// Often this is done as part of the Revit start-up process so that the updater is always active
public void RegisterUpdater()
{
    FamilyInstanceUpdater updater = new FamilyInstanceUpdater(this.Application.ActiveAddInId);
    UpdaterRegistry.RegisterUpdater(updater);

    // Trigger will occur only for FamilyInstance elements
    ElementClassFilter familyInstanceFilter = new ElementClassFilter(typeof(FamilyInstance));

    // GetChangeTypeElementAddition specifies that the triggger will occur when elements are added
    // Other options are GetChangeTypeAny, GetChangeTypeElementDeletion, GetChangeTypeGeometry, GetChangeTypeParameter
    UpdaterRegistry.AddTrigger(updater.GetUpdaterId(), familyInstanceFilter, Element.GetChangeTypeElementAddition());
}

public void UnregisterUpdater()
{
    FamilyInstanceUpdater updater = new FamilyInstanceUpdater(this.Application.ActiveAddInId);
    UpdaterRegistry.UnregisterUpdater(updater.GetUpdaterId());
}

I’ve tried to put a decent amount of description in the comments, for more info check out http://wikihelp.autodesk.com/Revit/enu/2013/Help/00006-API_Developer’s_Guide/0135-Advanced135/0152-Dynamic_152

Enhancement Idea:

  • Create a trigger that uses GetChangeTypeGeometry instead of GetChangeTypeElementAddition with a filter for Walls. If an existing wall that is hosting toilets becomes thinner (because its structure is changed or the wall type is changed) then the warning should be given.

Getting Parameter Data

For “info347074” who asked at AUGI about getting the values of shared parameters that are shown in labels on a sheet, here is a little macro showing how it can be done. Getting the parameter values from the sheets is no different than getting the values from any other Revit element.

CADFile Parameter


public void GetSheetParams()
{
    Document doc = this.ActiveUIDocument.Document;
    string sheetInfo = "";
    foreach (Element e in new FilteredElementCollector(doc).OfClass(typeof(ViewSheet)))
    {
        // get the parameter
        Parameter parameter = e.get_Parameter("CADFile");

        // get the string value of the parameter
        string cadFileData = parameter.AsString();

        sheetInfo += cadFileData + "\n";
    }
    TaskDialog.Show("CADFile", sheetInfo);
}

Catching Exceptions

As Arnost mentioned in a comment to an earlier post, the methods Selection.PickObject and Select.PickObjects throw exceptions when the user cancels the pick operation. This most commonly happens when the user presses the ESC key. Speaking of the ESC, did you read this http://www.nytimes.com/2012/10/07/magazine/who-made-that-escape-key.html?

If you run this macro and push ESC instead of making a selection, Revit will throw the exception, your code will not catch the exception, and you will see this:

Reference r = uidoc.Selection.PickObject(ObjectType.Element);
TaskDialog.Show("Thanks!", "You selected " + doc.GetElement(r).Name);

exception

Much better is for your code to handle the error by using a try/catch block. For example, this code displays a TaskDialog when the exception is thrown and then caught.

try
{
    Reference r = uidoc.Selection.PickObject(ObjectType.Element);
    TaskDialog.Show("Thanks!", "You selected " + doc.GetElement(r).Name);
}
catch (Autodesk.Revit.Exceptions.OperationCanceledException exception)
{
    TaskDialog.Show("Oh No!", "You did not make a selection so PickObject threw an exception: " + exception.Message);
}

catch

Retrieving Elements Stored in a SelectionFilterElement

Building on this previous post, here is how to retrieve elements that are stored in a SelectionFilterElement. After getting the elements, the code below shows how to select them in the Revit UI.
beforeAfter

public void GetSelectionFilterElements()
{
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = new UIDocument(doc);

    // Get the SelectionFilterElement named "My Selection Filter" with this LINQ statement
    SelectionFilterElement filter = (from f in new FilteredElementCollector(doc)
        .OfClass(typeof(SelectionFilterElement))
        .Cast<SelectionFilterElement>()
        where f.Name == "My Selection Filter" select f).First();

    // Get ids of elements in the filter
    ICollection<ElementId> filterIds = filter.GetElementIds();

    // Report # of elements to user
    TaskDialog.Show("Elements in filter", filterIds.Count.ToString());
    // ToString() must be used here because Count returns an integer and TaskDialog.Show requires a string
    // This hasn't been needed in other examples because when you concatenate a string with an integer
    // the whole thing automatically becomes a string. So ToString() would not be needed if I did:
    // "Number of elements is " + fitlerIds.Count

    // Now select the elements in "My Selection Filter" in the Revit UI
    // Create a SelElemenSet object
    SelElementSet selSet = SelElementSet.Create();
    foreach (ElementId id in filterIds) // loop through the elements in the filter
    {
        Element e = doc.GetElement(id); // get the element for each id
        selSet.Add(e); // add the element to the SelElementSet
    }
    // Use the SelElementSet to set the elements shown as selected in the Revit UI
    uidoc.Selection.Elements = selSet; 
    // Refresh the active view so that the change in selection higlighting is shown
    uidoc.RefreshActiveView();
}

Auto Generate 3D Walls from 2D Line floor plan (with a Selection pre-filter)

Darren asks at AUGI “Can Revit Architecture Auto Generate 3D from 2D floor plan?”

Here’s a little macro to do just that. Run the macro, select these 4 lines, and get these 4 walls.

walls from lines

Most of this should look familiar to code I have explained in previous posts.

The new piece is the ISelectionFilter. This applies a pre-filter to PickObjects so that when the user is prompted to select objects, the Select tool will only allow selections that pass the filter. In this case, the filter is defined so that it will return true only when ModelLines are selected. Nothing will be selected if the user clicks the mouse with the cursor over a wall, window, column, or anything else that it not a model line.

public void wallsFromLines()
{
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = new UIDocument(doc);

    // Get "Level 1"
    Level level = (from l in new FilteredElementCollector(doc).OfClass(typeof(Level)) where l.Name == "Level 1" select l).First() as Level;

    // Selection will be restricted to lines only. Attempts to click on other objects will be ignored
    ISelectionFilter lineFilter = new LineSelectionFilter();

    IList<Reference> lineRefs = uidoc.Selection.PickObjects(ObjectType.Element, lineFilter, "Pick lines");

    using (Transaction t = new Transaction(doc,"Create Walls From Lines"))
    {
        t.Start(); 
        foreach (Reference reference in lineRefs)
        {
            ModelLine modelLine = doc.GetElement(reference) as ModelLine;
            doc.Create.NewWall(modelLine.GeometryCurve, level, false); // false indicates that the wall's Structural property is false
        }
        t.Commit();
    }
}
public class LineSelectionFilter : ISelectionFilter
{
    // determine if the element should be accepted by the filter
    public bool AllowElement(Element element)
    {
        // Convert the element to a ModelLine
        ModelLine line = element as ModelLine;
        // line is null if the element is not a model line
        if (line == null)
        {
            return false;
        }
        // return true if the line is a model line
        return true;
    }

    // references will never be accepeted by this filter, so always return false
    public bool AllowReference(Reference refer, XYZ point)
    {return false;}
}

In the real world you may want to do this with many walls and use a filter or some other mechanism instead of selecting them individually. You would also want something more sophisticated to specify the base constraint of the walls instead of having them all at Level 1. Model lines don’t have a Level property, but you can get their Z location from ModelLine.GeometryCurve.Origin and then find a level with the same elevation.

Macro to determine if a Family is In-Place (with output to Text and Excel Files)

A recent post at whatrevitwants mentions some tools that can be used to find in-place families. Here is a tiny macro to do the same

public void findInPlaceFamilies()
{
    string info = "";
    Document doc = this.ActiveUIDocument.Document;
    foreach (Family family in new FilteredElementCollector(doc).OfClass(typeof(Family)).Cast<Family>())
    {
        if (family.IsInPlace)
            info += family.Name + "\n";
    }
    TaskDialog.Show("In Place Families", info);
}

This is also a good excuse to show how to do something other than show the string inside Revit in a TaskDialog. For example, we might want to write the data to a text file or to an Excel file.

Text File:

public void findInPlaceFamilies()
{
    Document doc = this.ActiveUIDocument.Document;

    // Use the @ before the filename to avoid errors related to the \ character
    // Alternative is to use \\ instead of \
    string filename = @"C:\Users\HP002\Documents\In Place Families.txt";

    // Create a StreamWriter object to write to a file
    using (System.IO.StreamWriter writer = new System.IO.StreamWriter(filename))
    {
        foreach (Family family in new FilteredElementCollector(doc).OfClass(typeof(Family)).Cast<Family>())
        {
            if (family.IsInPlace)
                writer.WriteLine(family.Name);
        }
    }    
    // open the text file
    System.Diagnostics.Process.Start(filename);
}

Excel File:

  1. Select “Project – Add Reference” from the SharpDevelop menu bar and add a reference to Microsoft.Office.Interop.Excel
    AddExcelReference
  2. Add these lines to the other “using” statements at the top of your file:
    using Microsoft.Office.Interop.Excel;
    using System.Reflection;
  3. Adding “Microsoft.Office.Interop.Excel” may result in errors like this if elsewhere in your macros you are using the Revit Parameter class:
    Ambiguous
    This happens because now you can no longer define variables as “Parameter” because both Autodesk.Revit.DB and Microsoft.Office.Interop.Excel contain definitions for “Parameter”. The compiler can’t guess which one you want to use.
    Go through these errors and replace “Parameter” with  “Autodesk.Revit.DB.Parameter” like this:

    Autodesk.Revit.DB.Parameter parameter = element.get_Parameter("Material");
    
public void findInPlaceFamilies()
{
    Document doc = this.ActiveUIDocument.Document;

    // set up Excel variables
    Application excelApp = new Application();
    Workbook workbook = excelApp.Workbooks.Add(Missing.Value);
    Worksheet worksheet = (Worksheet)workbook.ActiveSheet;

    string filename = @"C:\Users\HP002\Documents\In Place Families.xls";
    int rowCtr = 1;
    foreach (Family family in new FilteredElementCollector(doc).OfClass(typeof(Family)).Cast<Family>())
    {
        if (family.IsInPlace)
        {
            // write data to the next row in the spreadsheet
            worksheet.Cells[rowCtr,1] = family.Name;
            rowCtr++; // increment counter by one
        }
    }

    // Save the file
    workbook.SaveAs(filename,XlFileFormat.xlWorkbookNormal, 
        Missing.Value, Missing.Value, Missing.Value, Missing.Value, 
        XlSaveAsAccessMode.xlExclusive, Missing.Value, Missing.Value, Missing.Value, 
           Missing.Value, Missing.Value); 
    // Clean up the excel resources that were created
    workbook.Close();
    excelApp.Quit();

    System.Diagnostics.Process.Start(filename);
}

Don’t mind all that crazy “Missing.Value” stuff. The Excel API is strange but it works.

Making Lines Not Print Improved (View Viz instead of Line Color)

David makes a great observation regarding my post at https://boostyourbim.wordpress.com/2012/12/12/making-lines-not-print-with-events/

‘That all works great if the background is in fact white. What happens if you have the same situation you were showing above with a floor tile pattern? You’d see the white line.”

The code I wrote in that post was I hope good for a relatively gentle introduction to the concept of Revit API Events. But this is not what anyone wants to see when they print.

whiteonpattern

I used the approach of changing the Object Styles – Line Color to white because it offered a straightforward way to globally change the appearance of the lines in all views. A better approach that requires greater API complexity could be to turn off the visibility of the Do Not Print sub-category in each view that is being printed.

vis

In the previous post’s code, myPrintingEvent and myPrintedEvent both had input arguments DocumentPrintingEventArgs and DocumentPrintedEventArgs that went unused. Every event has arguments like these that are used to pass data about what is happening in this event. For the printing events, this information includes the list of views to be printed and the list of views that were printed. This will be needed for the new approach of changing  category visibility for each view that is printed.

The structure of the code is the same as the previous post, but the CategoryLineColor method has been replaced with categoryVisInViews which uses View.SetVisibility(category, boolean).

public void PrintingEventRegister()
{
    // myPrintingEvent will run just BEFORE Revit prints
    this.Application.DocumentPrinting += new EventHandler<DocumentPrintingEventArgs>(myPrintingEvent);

    // myPrintedEvent will run just AFTER Revit prints
    this.Application.DocumentPrinted += new EventHandler<DocumentPrintedEventArgs>(myPrintedEvent);
}

private void myPrintingEvent(object sender, DocumentPrintingEventArgs args)
{
    // turn OFF visibility of the ""Generic Annotations - Do Not Print" subcategory in the views being printed
    categoryVisInViews(args.Document, args.GetViewElementIds(), false);
}

private void myPrintedEvent(object sender, DocumentPrintedEventArgs args)
{
    // list of views that both printed and failed to print
    List<ElementId> views = new List<ElementId>();
    views.AddRange(args.GetFailedViewElementIds());
    views.AddRange(args.GetPrintedViewElementIds());

    // turn ON visibility of the ""Generic Annotations - Do Not Print" subcategory in views that were printed & that failed to print
    categoryVisInViews(args.Document, views, true);
}

// This function takes as inputs:
// 1) The document
// 2) The list of views in which the "Do Not Print" subcategory visibility should be changed
// 3) A boolean (true or false) indicating the desired state of the category visibility 
private void categoryVisInViews(Document doc, IList<ElementId> viewList, bool visible)
{
    foreach (ElementId id in viewList)
    {
        View view = doc.GetElement(id) as View;
        // Get all categories in the document
        Categories categories = doc.Settings.Categories;

        // The "Generic Annotations" category
        Category genericAnnotations = categories.get_Item("Generic Annotations");

        // The ""Do Not Print" subcategory of the "Generic Annotations" category
        Category doNotPrint = genericAnnotations.SubCategories.get_Item("Do Not Print");

        // Create transaction with the view name & state in the transaction name
        using (Transaction t = new Transaction(doc,"Set Visibility: " + view.Name + ": " + visible))
        {            
            t.Start();
            // set the visibility of the category
            view.setVisibility(doNotPrint, visible);
            t.Commit();
        }            
    }
}