#RTCEUR Wish 3 – Copy View Types & Templates btw Documents

Q: Would be possible to create a tool that would “Transfer View Types” from one project to another?

Background: View Types have a couple of key properties, an assigned View Template, and if new view instances created from the Type should be dependent on the View Template. But the Revit UI does not allow view types to be transferred between projects.

A: Good news! With the API it is possible to determine the View Types defined in a project, read their associated parameters, create new View Types in a second Project, and set these parameters to match the values in the source project.

public void CopyViewTypes()
{
    Application app = this.Application;

    // source document is the active document
    Document sourceDoc = this.ActiveUIDocument.Document;

    // target document is an open file named "target.rvt"
    Document targetDoc = app.Documents.Cast<Document>().Where(q => Path.GetFileName(q.PathName) == "target.rvt").FirstOrDefault();

    // element ids for all instances of the ViewFamilyType class
    ICollection<ElementId> viewFamilyTypeIds = new FilteredElementCollector(sourceDoc).OfClass(typeof(ViewFamilyType)).ToElementIds();

    // element ids of the view templates used in View Types
    ICollection<ElementId> templateIds = new List<ElementId>();

    // list of pairs of strings - first item in each pair will be the name of the View Type - second item will be the name of the View Template used by that View Type 
    IList<Tuple<string, string>>  ssList = new List<Tuple<string, string>>();

    foreach (ViewFamilyType vft in new FilteredElementCollector(sourceDoc).OfClass(typeof(ViewFamilyType)))
    {
        // element id of the view template used in this View Type
        ElementId defaultTemplateId = vft.get_Parameter(BuiltInParameter.DEFAULT_VIEW_TEMPLATE).AsElementId();

        // go to the next View Type if there is no View Template assigned to this view type
        if (defaultTemplateId == ElementId.InvalidElementId)
            continue;

        templateIds.Add(defaultTemplateId);

        ssList.Add(new Tuple<string, string>(vft.Name, sourceDoc.GetElement(defaultTemplateId).Name));
    }

    CopyPasteOptions options = new CopyPasteOptions();

    using (Transaction t = new Transaction(targetDoc, "Copy View Types"))
    {
        t.Start();
        // copy the view templates
        ElementTransformUtils.CopyElements(sourceDoc, templateIds, targetDoc, Transform.Identity, options);
        // copy the view types
        ElementTransformUtils.CopyElements(sourceDoc, viewFamilyTypeIds, targetDoc, Transform.Identity, options);

        foreach (Tuple<string, string> tup in ssList)
        {
            Element vft = new FilteredElementCollector(targetDoc).OfClass(typeof(ViewFamilyType)).Where(q => q.Name == tup.Item1).FirstOrDefault();

            View template = new FilteredElementCollector(targetDoc).OfClass(typeof(View)).Cast<View>().Where(q => q.IsTemplate && q.Name == tup.Item2).FirstOrDefault();

            // assign the view template to the view type
            vft.get_Parameter(BuiltInParameter.DEFAULT_VIEW_TEMPLATE).Set(template.Id);
        }

        t.Commit();
    }
}
Advertisements

#RTCEUR Wish 2 – Highlight and Isolate Warning Elements

@marcellosgamb suggested an API that runs thru every error in a model turns element w/error red then as u fix each error turn normal

There is no API access to the Revit “Review Warnings” data, so this implementation requires the user to export the warning data. The program then imports this HTML, parses it to find the element IDs, and then isolates and colors the elements in two 3D views.

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

    string filename = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "warnings.html");

    IList<ElementId> ids = new List<ElementId>();

    using (StreamReader sr = new StreamReader(filename))
    {
        string line = "";
        while ((line = sr.ReadLine()) != null)
        {
            string[] row = System.Text.RegularExpressions.Regex.Split(line,"id ");
            if (row.Length != 3)
            {
                continue;
            }
            string id1 = row[1].Split(' ')[0];
            string id2 = row[2].Split(' ')[0];

            ids.Add(new ElementId(int.Parse(id1)));
            ids.Add(new ElementId(int.Parse(id2)));
        }
    }    

    View isolateView = new FilteredElementCollector(doc).OfClass(typeof(View)).Cast<View>().Where(q => q.Name == "IsolateWarningElements").FirstOrDefault();
    View overrideView = new FilteredElementCollector(doc).OfClass(typeof(View)).Cast<View>().Where(q => q.Name == "OverrideWarningElements").FirstOrDefault();

    OverrideGraphicSettings ogs = new OverrideGraphicSettings();
    Color red = new Color(255, 0, 0);
    ogs.SetProjectionLineColor(red);
    ogs.SetProjectionLineWeight(8);

    Element solidFill = new FilteredElementCollector(doc).OfClass(typeof(FillPatternElement)).Where(q => q.Name.Contains("Solid")).First();

    ogs.SetProjectionFillPatternId(solidFill.Id);
    ogs.SetProjectionFillColor(new Color(0, 255,0));

    using (Transaction t = new Transaction(doc, "Isolate/Override Warnings"))
    {
        t.Start();
        isolateView.IsolateElementsTemporary(ids);

        foreach (ElementId id in ids)
        {
            overrideView.SetElementOverrides(id, ogs);
        }
        t.Commit();
    }
}

#RTCEUR Wish #1 – Push parameter from Host to Hosted

@Jbenoit44 suggested a tool to push a param from any Host (inc. Lev. and ref pl.) to any Hosted & reverse. Doors windows face base fam Parts etc

This implementation drives parameters from Hosts to their Family Instances.

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

    ElementCategoryFilter hostFilter = new ElementCategoryFilter(BuiltInCategory.OST_Walls);
    ElementCategoryFilter hostedFilter = new ElementCategoryFilter(BuiltInCategory.OST_Doors);

    string parameterName = "Rating";

    using (Transaction t = new Transaction(doc, "Set hosted parameters"))
    {
        t.Start();
        foreach (Element host in new FilteredElementCollector(doc).WherePasses(hostFilter))
        {
            Parameter paramHost = host.get_Parameter(parameterName);

            foreach (Element hosted in new FilteredElementCollector(doc)
                     .WherePasses(hostedFilter)
                     .OfClass(typeof(FamilyInstance))
                     .Cast<FamilyInstance>()
                     .Where(q => q.Host.Id == host.Id))
            {
                if (paramHost.StorageType == StorageType.String)
                    hosted.get_Parameter(parameterName).Set(paramHost.AsString());
                else if (paramHost.StorageType == StorageType.Double)
                    hosted.get_Parameter(parameterName).Set(paramHost.AsDouble());
                else if (paramHost.StorageType == StorageType.Integer)
                    hosted.get_Parameter(parameterName).Set(paramHost.AsInteger());
            }
        }
        t.Commit();
    }
}

Deleting DWG Imports and their categories

Recent posts have looked at how to delete ImportInstance elements. This led to the question about how to get rid of those categories that are left behind and visible in the Visibility/Graphics and Object Styles dialog.

Capture

In some ways, import instances and family instances are quite similar. Revit stores a single definition of the family/import and then places one or more instances of the family/import in the model.

With the FamilyInstance class in the API we can get the FamilySymbol and from the FamilySymbol we can get the Family. ImportInstance has no such properties. But we can see from the status prompt & tooltip when these elements are selected that there is a similar hierarchy in both FamilyInstance and ImportInstance elements.

Untitled

The solution is that if you want to delete both the import instances and the associated import categories, then the thing do do is delete the category, not just the instance.

This code builds a unique list of element id’s that represent the categories of imported elements.  Deleting the import category also deletes all instances of that import.

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

    IList<ElementId> categoryIds = new List<ElementId>();

    foreach (ImportInstance ii in  new FilteredElementCollector(doc)
        .OfClass(typeof(ImportInstance))
        .Cast<ImportInstance>()
        .Where(i => i.IsLinked == false))
    {
        ElementId catId = ii.Category.Id;
        if (!categoryIds.Contains(catId))
            categoryIds.Add(catId);
    }

    using (Transaction t = new Transaction(doc,"Delete Import Categories"))
       {
        t.Start();
        doc.Delete(categoryIds);
        t.Commit();
    }
}

Topo From Lines – App #3 submitted to the Autodesk Port-a-thon

Mike suggested

Something with the new topo API would be cool. I was thinking if you could draw topo lines in Revit and give them a height, that the toposurface could be created from that, instead of drawing lines in cad with elevation and importing to create the surface.

So I made this:

Keynote Reporter – App #2 submitted to the Autodesk Port-a-thon

Nathan suggested:

Something that will highlight any keynotes that have lost their host and show up as blank. Actually a tool that highlights User Keynotes versus the other types would be cool, too. We don’t like User Keynotes in my office, but many people still insist on using them for some reason. Being able to pick out how many there are in a given view would be useful.

So here is the output from the Keynote Reporter that I just submitted to Autodesk’s App-A-Thon. You can use the semicolon-delimited list of element IDs along with Revit’s “Select By ID” tool to select groups of tags based on their type or orphan-status.

 

 

KeynoteReportScreenShot

The LINQ “Select” statement to get rid of extraneous looping

In the previous post, I included this code to show how to build a list of element id’s that could then be used with Document.Delete

It finds all ImportInstance elements that are not linked, and then iterates through those elements in the foreach loop to create a list of their IDs.

    IList<ElementId> toDelete = new List<ElementId>();
    foreach (ImportInstance ii in new FilteredElementCollector(doc)
         .OfClass(typeof(ImportInstance))
         .Cast<ImportInstance>()
         .Where(i => i.IsLinked == false))
        {
            toDelete.Add(ii.Id);
        }   

But that foreach loop looks ugly (to me) and so I wanted to share a simplification that allows you to get rid of that loop and build this list of IDs directly.

The LINQ Select statement takes a lambda expression (in this case an expression taking a type ImportInstance represented as i). It returns whatever you want it to, in this case the ElementId returned from the i.Id property.

    IList<ElementId> toDelete =  new FilteredElementCollector(doc)
        .OfClass(typeof(ImportInstance))
        .Cast<ImportInstance>()
        .Where(i => i.IsLinked == false)
        .Select(i => i.Id)
        .ToList();

Looped Deletion of Elements in 2014

The code below would seem like a reasonable way to delete a set of elements returned by the FilteredElementCollector

public void deleteImportsNotLinks_Does_Not_Work_In_2014()
{
    Document doc = this.ActiveUIDocument.Document;
    using (Transaction t = new Transaction(doc,"Delete Imports"))
       {
        t.Start();
        foreach (ImportInstance ii in new FilteredElementCollector(doc)
                 .OfClass(typeof(ImportInstance))
                 .Cast<ImportInstance>()
                 .Where(i => i.IsLinked == false))
        {
            doc.Delete(ii.Id);
        }      
        t.Commit();
   }
}

But if you try this in 2014 you get this error.

Capture

This tells us that the FilteredElementCollector is unhappy because we deleted an element, which changed Revit’s database, which then causes problems when Revit tries to go to the next element that would be returned from the FilteredElementCollector.

The solution is to split the code into two parts

  1. Build a list of the element ids to delete (called ‘toDelete’ in the code sample below)
  2. Delete the elements in this list

Like this:

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

    IList<ElementId> toDelete = new List<ElementId>();
    foreach (ImportInstance ii in new FilteredElementCollector(doc)
         .OfClass(typeof(ImportInstance))
         .Cast<ImportInstance>()
         .Where(i => i.IsLinked == false))
        {
            toDelete.Add(ii.Id);
        }   

    using (Transaction t = new Transaction(doc,"Delete Imports"))
       {
        t.Start();
        doc.Delete(toDelete);  
        t.Commit();
    }
}

UPDATE: As Philip noted in the comments, this is a good opportunity to use the overload of Document.Delete that takes an ICollection(ElementId) as its input as shown in the updated code sample. Check out the next post for an even fancier way to do this.

What sheets are my views on?

A reader asked: How do u find out what sheet a view is on via API?

Below is some code and the screenshot shows the Project Browser and output from the API macro.

ViewSheetReport

public void viewSheetReport()
{
    Document doc = this.ActiveUIDocument.Document;
    string data = "";
    foreach (View v in new FilteredElementCollector(doc).OfClass(typeof(View)))
    {
        string thisSheet = "";
        foreach (ViewSheet vs in new FilteredElementCollector(doc)
            .OfClass(typeof(ViewSheet))
            .Cast<ViewSheet>())
        {
            foreach (View view in vs.Views)
            {
                if (view.Id == v.Id)
                {
                    thisSheet += vs.Name + " - " + vs.SheetNumber + ", ";
                    break;
                }
            }
        }
        if (thisSheet != "")
        {
            data += v.ViewType + " - " + v.Name + ": " + thisSheet.TrimEnd(' ',',') + Environment.NewLine + Environment.NewLine;
        }
    }
    TaskDialog.Show("Sheet Report", data);
}

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