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");
}
Month: December 2012
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");
}
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)
2) Right click on the References folder and select “Add Reference”
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.
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.
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!
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′
- Find the glass area of the door as-is
- Edit the door family
- Create a void to eliminate the bottom 2′ of glass
- Reload the door into the project
- Find the glass area of the modified door
- Rollback the changes to the door and the reloading of the door into the RVT
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”.
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.
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.
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);
}
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.
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.
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);
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);
}
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.
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.
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:
- Select “Project – Add Reference” from the SharpDevelop menu bar and add a reference to Microsoft.Office.Interop.Excel
- Add these lines to the other “using” statements at the top of your file:
using Microsoft.Office.Interop.Excel; using System.Reflection;
- Adding “Microsoft.Office.Interop.Excel” may result in errors like this if elsewhere in your macros you are using the Revit Parameter class:
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.
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.
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();
}
}
}