Copy a sheet from one project to another

In response to my post “Transferring just one View Template from Project to Project“, Mike asks:

How would you implement this to copy sheets?

To start, it is not possible to copy viewports with the API. This is not too much of a surprise, as the Revit UI does not allow duplicating sheets and does not allow copying viewports. Also, it is not possible to copy a sheet that has viewports on it.

However, it is possible to use the API to copy the sheet, titleblock, and view-specific elements on the sheet if we use this procedure:

  1. Temporarily delete the viewports from the sheet (this is done in a transaction that be rolled back at the end so that the viewports are not actually deleted)
  2. Copy the sheet to the new project
  3. Copy the view-specific elements (excluding the viewports) from the source sheet view to the destination sheet view

The sneaky part of this is realizing that that while a single document can have only one transaction open at any given time, we can have one transaction in each document open at the same time.

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

    ViewSheet activeViewSheet = doc.ActiveView as ViewSheet;
    if (activeViewSheet == null)
    {
        TaskDialog.Show("Error", "Active view must be a sheet.");
        return;
    }

    Application app = this.Application;
    Document otherDoc = app.Documents.Cast<Document>().Where(d => d.Title != doc.Title).FirstOrDefault();
    if (otherDoc == null)
    {
        TaskDialog.Show("Error", "There must be a 2nd document open.");
        return;
    }

    // put the sheet in the source document into the copyIds collection
       ICollection<ElementId> copyIds = new Collection<ElementId>();
    copyIds.Add(activeViewSheet.Id);    

    // put view-specific elements on the sheet in the copyIdsViewSpecific collection
    ICollection<ElementId> copyIdsViewSpecific = new Collection<ElementId>();
    foreach (Element e in new FilteredElementCollector(doc).OwnedByView(doc.ActiveView.Id))
    {
        // do not put viewports into this collection because they cannot be copied
        if (!(e is Viewport))
            copyIdsViewSpecific.Add(e.Id);    
    }

    // Create a transaction in the source document to delete the viewports.
    // This transaction will be rolled-back so it won't cause any permanent change in the document
    // but it will enable copying of the sheet while it is in a state with no viewports
    using (Transaction t = new Transaction(doc,"Delete Viewports"))
    {
        t.Start();

        IList<Viewport> viewports = new FilteredElementCollector(doc).OfClass(typeof(Viewport)).Cast<Viewport>()
            .Where(q => q.SheetId == activeViewSheet.Id).ToList();

        foreach (Viewport vp in viewports)
        {
            doc.Delete(vp.Id);
        }

        using (Transaction tOther = new Transaction(otherDoc, "Copy View Template"))
        {
            tOther.Start();
            // copy the sheet using the CopyElements overload that accepts source and destination documents
            // get the newly created sheet in the target document - it will be the first (and only) element returned by ElementTransformUtils.CopyElements
            ViewSheet newSheet = otherDoc.GetElement(ElementTransformUtils.CopyElements(doc, copyIds, otherDoc, Transform.Identity, new CopyPasteOptions()).First()) as ViewSheet;

            // copy the view-specific elements using the CopyElements overload that accepts source and destination views
            ElementTransformUtils.CopyElements(activeViewSheet, copyIdsViewSpecific, newSheet, Transform.Identity, new CopyPasteOptions());

            tOther.Commit();
        }

        // rollback the transaction to "undo" the deletion of the viewports
        t.RollBack();
    }
}

Net and Gross Wall Area

At Revit Forum it was asked how to get the gross area of a wall. Here’s a bit of code using Document.Regeneration() and Transaction.Rollback() to get the job done.

Capturepublic void NetWallArea()
{
    UIDocument uidoc = this.ActiveUIDocument;
    Document doc = uidoc.Document;
    foreach (Wall w in new FilteredElementCollector(doc).OfClass(typeof(Wall)).Cast<Wall>())
    {
        // get a reference to one of the wall's side faces
        Reference sideFaceRef = HostObjectUtils.GetSideFaces(w, ShellLayerType.Exterior).First();

        // get the geometry object associated with that reference
        Face netFace = w.GetGeometryObjectFromReference(sideFaceRef) as Face;

        // get the area of the face - this area does not include the area of the inserts that cut holes in the face
        double netArea = netFace.Area;

        double grossArea;
        using (Transaction t = new Transaction(doc,"delete inserts"))
        {
            t.Start();

            // delete all family inserts that are hosted by this wall
            foreach (FamilyInstance fi in new FilteredElementCollector(doc).OfClass(typeof(FamilyInstance)).Cast<FamilyInstance>().Where(q => q.Host != null && q.Host.Id == w.Id))
            {
                doc.Delete(fi.Id);
            }
            // regenerate the model to update the geometry with the inserts deleted
            doc.Regenerate();

            // get the gross area (area of the wall face now that the inserts are deleted)
            Face grossFace = w.GetGeometryObjectFromReference(sideFaceRef) as Face;
            grossArea = grossFace.Area;

            // rollback the transaction to restore the model to its original state
            t.RollBack();
        }
        TaskDialog.Show("Areas", "Net = " + netArea + "\nGross = " + grossArea);
    }
}

Two Clicks to Create and Rotate Family Instances

public void createAndOrient()
{
    UIDocument uidoc = this.ActiveUIDocument;
    Document doc = uidoc.Document;
    Application app = doc.Application;

    // Get the family symbol named "North Arrow 2"
    FamilySymbol famSym = new FilteredElementCollector(doc).OfClass(typeof(FamilySymbol)).Where(q => q.Name == "North Arrow 2").First() as FamilySymbol;

    // use a transaction group so that all the individual transactions are merged into a single entry in the Undo menu
    // this is optional
    using (TransactionGroup tg = new TransactionGroup(doc,"Create and Orient Instances"))
    {
        tg.Start();
        // create an infinite loop so user can create multiple instances in a single command
        // ESC when prompted to select a point will thrown an exception which is how the loop is exited
        while (true)
        {
            try
            {                
                XYZ pickPoint = uidoc.Selection.PickPoint("Click to specify instance location. ESC to stop placing instances.");

                FamilyInstance familyInstance = null;    
                // Create the instance with the default orientation
                // This is done in its own transaction so that the user can see the new instance when they are prompted for the orientation
                using (Transaction t = new Transaction(doc,"Place Instance"))
                {
                    t.Start();
                    familyInstance = doc.Create.NewFamilyInstance(pickPoint, famSym, doc.ActiveView);
                    t.Commit();
                }

                XYZ orientPoint = uidoc.Selection.PickPoint("Click to specify orientation. ESC to stop placing instances.");

                // Create a line between the two points
                // A transaction is not needed because the line is a transient element created in the application, not in the document
                Line orientLine = app.Create.NewLineBound(pickPoint, orientPoint);

                // Compute the angle between the vertical direction (XZY.BasisY) and the orientLine
                double angle = XYZ.BasisY.AngleTo(orientLine.Direction);

                // For diagnostics in Task dialog below
                double angleDegrees = angle * 180 / Math.PI;

                // AngleTo always returns the smaller angle between the two lines (for example, it will always return 10 degrees, never 350)
                // so if the orient point is to the left of the pick point, then correct the angle by subtracting it from 2PI (Revit measures angles in degrees)
                if (orientPoint.X < pickPoint.X)
                    angle = 2 * Math.PI - angle;

                // To show the need for angle corrections
                double angleDegreesCorrected = angle * 180 / Math.PI;
                //TaskDialog.Show("info","Angle directly from AngleTo = " + angleDegrees + "\n Angle after X correction = " + angleDegreesCorrected);

                // Create an axis in the Z direction 
                Line axis = app.Create.NewLineBound(pickPoint, new XYZ(pickPoint.X, pickPoint.Y, pickPoint.Z + 10));

                using (Transaction t = new Transaction(doc,"Orient Instance"))
                {
                    t.Start();
                    ElementTransformUtils.RotateElement(doc, familyInstance.Id, axis, -angle);
                    t.Commit();
                }
            }
            catch
            {
                // Get here when the user hits ESC when prompted for selection
                // "break" exits from the while loop
                break;
            }
        }
    // Consolidate all the transactions for the individual creation / rotation transactions into a single Undo item	
    tg.Assimilate();
    }
}

Dynamic Model Update, Exceptions, and Transactions (hint – look in the journal file)

In the last post I casually mentioned that during Dynamic Model Update we could delete the just-created elements. My first attempt was to add a transaction and Document.Delete(). That doesn’t work, and it also brings up a useful tip.

The error dialog shown on the right is Revit’s way of saying that your updater threw an exception, though it doesn’t give any technical details about the nature of the execption. “But what was the exception?” you will rightfully wonder! The answer is in the journal file, where in this case it explains:

' 2:< Exception caught from managed method RevitAPI::Autodesk.Revit.DB.TransactionStatus Start() <Autodesk.Revit.Exceptions.InvalidOperationException> <Starting a new transaction is not permitted. It could be because another transaction already started and has not been completed yet, or the document is in a state in which it cannot start a new transaction (e.g. during failure handling or a read-only mode, which could be either permanent or temporary).>

errorpublic void Execute(UpdaterData data)
{
    Document doc = data.GetDocument();
    Autodesk.Revit.ApplicationServices.Application app = doc.Application;
    foreach (ElementId addedElemId in data.GetAddedElementIds())
    {
        ImportInstance ii = doc.GetElement(addedElemId) as ImportInstance;
        if (ii.IsLinked == false)
            TaskDialog.Show("Hey!", app.Username + " - Maybe should should have linked that CAD instead of importing it.");

        // THIS DOESN'T WORK!
        Transaction t = new Transaction(doc,"delete that");
        t.Start();
        doc.Delete(ii);
        t.Commit();
    }
}

Thanks to that info in the journal file, we learn that we need to get rid of the transaction code. Doing so gives us this, which works fine to delete the import instance that was just created.

public void Execute(UpdaterData data)
{
    Document doc = data.GetDocument();
    Autodesk.Revit.ApplicationServices.Application app = doc.Application;
    foreach (ElementId addedElemId in data.GetAddedElementIds())
    {
        ImportInstance ii = doc.GetElement(addedElemId) as ImportInstance;
        if (ii.IsLinked == false)
            TaskDialog.Show("Hey!", app.Username + " - Maybe should should have linked that CAD instead of importing it.");
        doc.Delete(ii);
    }
}

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

Sometimes in order to create, you have to destroy

To follow up with another post about directionality, I wanted to first introduce the important concept of using the Delete method for things other than deleting objects.

One of the valuable applications is when we want to find the doors or windows in a wall. For any given hosted family instance we can use the FamilyInstance.Host property to find the wall that hosts it.

But what if we want to go the other way, and start with a wall and find the elements hosted by it? A great way to do this is delete the wall and get the data that Revit provides about all items that were deleted (because deleting the wall also deletes its doors & windows).

But wait! You just messed up my model by deleting a wall!

No, not really. This deletion of the wall does will not be committed to the Revit model and we can still get the info we want. By using Transaction.Rollback (instead of Transaction.Commit) the wall was never really deleted.

Here’s an example that finds the doors in the selected wall. For the screenshot I selected the horizontal wall, and the dialog box shows only the doors in that wall (note that the double door in the vertical wall is not listed).

doors in wall

public void FindDoorsInWall()
{
    Document doc = this.ActiveUIDocument.Document;
    UIDocument uidoc = new UIDocument(doc);
    Wall wall = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element)) as Wall;
    List<ElementId> wallAndChildren = new List<ElementId>();
    using( Transaction transaction = new Transaction(doc,"Delete Wall") )
    {
      transaction.Start();
      wallAndChildren = doc.Delete(wall) as List<ElementId>;
      // roll back the tranaction instead of using transaction.Commit()
      // we don't really want to delete the wall
      transaction.RollBack();
    }
    string doors = "";
    foreach (ElementId id in wallAndChildren)
    {
        FamilyInstance famInst = doc.GetElement(id) as FamilyInstance;
        if (famInst != null && famInst.Category.Name == "Doors")
        // need this check to exclude the wall that was deleted
        // the check for null determines that the element is a FamilyInstance					
        {
            FamilySymbol familySymbol = famInst.Symbol;
            Family family = familySymbol.Family;
            doors += family.Name + " - " + famInst.Name + "\n";
        }
    }
    TaskDialog.Show("doors in selected wall", doors);
}

Constructors and creating a new transaction

A reader asked for some more information about this line of code:

Transaction t = new Transaction(document,”Modify Mark values”);

What is happening here is that a new Transaction object is being created using the Transaction constructor. The transaction must be associated with a document (you can have several open documents co-existing simultaneously in your Revit API code).

You will also need to give your constructor a name. This can be done when the transaction is created as above, or you can use the constructor that requires only a document and specify the name later using the Transaction.SetName() method. The name of the transaction can be any string that you want. If the transaction is committed (which means that it successfully completed) then the end-user will see the name of the transaction in the Undo menu.

Creating a transaction does not automatically start the transaction. That must be done separately with the Start method. When you are finished with the changes to the model, use the Commit method. Or you can use the Rollback method if you don’t want to commit the changes that were made after starting the transaction.

 

Use a Using block with Transactions

The code I originally had below in the Duplicate Mark sample works fine when everything goes according to plan. But a more robust approach would use a “using” block as shown below.

using( Transaction transaction = new Transaction(document,"action") )
{
   transaction.Start();
  // ....
   transaction.Commit();
}

This goes for every transaction as well as every transaction group and sub-transaction. The objective is to make sure those objects are properly closed (particularly in case of an exception). More on the subject is in the MSDN C# reference.

Resolving duplicate tags Step 4 – transactions and wrapping up

We’re almost done!

The last thing to mention is that any time we use the API to modify the document, we need to first create and start a transaction. When we are finished making changes we commit the transaction. Here is the final code with the transactions added:

public void MakeMarksUnique()
{
    Document document = this.ActiveUIDocument.Document;
    FilteredElementCollector collector = new FilteredElementCollector(document);
    IList<Element> doorList = collector.OfCategory(BuiltInCategory.OST_Doors).OfClass(typeof(FamilyInstance)).ToList();
    if (doorList.Count > 0)
    {
        IList<string> markValues = new List<string>();
        using (Transaction t = new Transaction(document,"Modify Mark values"))
        {
            t.Start();           
            foreach (Element e in doorList)
            {
                Parameter p = e.get_Parameter("Mark");
                if (markValues.Contains(p.AsString()))
                    p.Set(p.AsString() + "*");
                else
                    markValues.Add(p.AsString());
            }
            t.Commit();
        }
    }
}

And here is what the Revit model looks like after running the command. Note that three of the doors have new mark values and the Undo menu includes the “Modify Mark Values” transaction that was created by the macro.

noduplicates

I hope you enjoyed this first little project and found it interesting and useful. Please leave comments about what you think I should do next!