[Tag Plans by Sheet]
One of the most useful things Dynamo can do, in my opinion, is task automation. It has given us the opportunity to whip up a quick, simple script in 5, maybe 10 minutes, which can end up saving us hours if not days. This third module is one such example.
The idea is simple, search through a list of sheets and their views to tag an element of specified category. This is useful in a situation where you may have far too many sheets and not enough time to go through every view tagging every other element. It essentially uses the interface Tag all by Category function but applies it to a large number of views, not just one.
This script focuses on plans for the reason that tagging plans and tagging sections or elevation views are two different beasts. You could very well tweak this script to tag elevations but you might end up having elements being tagged which are not in view but in the crop box. Therefore, I’ve decided to address that problem in another module and focus on plans for this module. Hope you enjoy it and please let me know if you make any nice changes or adaptions that might be useful for others.
The first step for this script is the gather up all the sheets that we want to tag. The quickest way to do this is by the Python Script Node shown below in 1.1. Alternatively you could use the Sheet Collector node in the Lunchbox package however, I have used a python script to show you exactly how it is done. The Boolean node is used to reset the Python Script if needed, as shown in prior modules.
The output of the Python Script node is a list with three sub-lists. The first sub-list contains sheets as elements, retrieved using List.FirstItem node, the second sub-list contains the sheet names, retrieved using List.GetItemAtIndex and index 1, and the last sub-list contains the sheet numbers, retrieved using List.LastItem. As you can see in the Watch nodes, the lists come out in the same order so we now have three lists including the sheet,names and numbers in the same order.
In this particular run of the script, I am using the list of sheet numbers to search for views however, this can be to your choosing which means you can search by sheet name or number. To do this , simply change the list entering the String.Contains node to whichever you like. In this example, I have used the sheet number A100 in the Sheet String node. The String.Contains node will then search all the sheets in the list that is fed in to the node to find those that contain that sheet number and return True or False. The Boolean node is used here to make sure String.Contains does not ignore upper and lowercase differences.
Now that we have a list of booleans from String.Contains node, we can compare that against the list of sheet elements to filter out only the sheets we want to search. The List.FilterByBoolMask helps us achieve this. The input for this node is the list of sheet elements, through the list input and the list of booleans through the mask input. What this does is it looks at each item in the list input and checks the corresponding index of the mask list. If the mask list index is True, it will append that corresponding index from the list input to the in output. Therefore, we now have a list of sheets that contain the number A100 in their title which, in this case, is 1 which is visible in the Watch node.
With this list, we can use the Sheet.Views node to get all the views and then Flatten these views into one list.
The only input for this script is the toggle which allows us to reset the script if needed. Four lists are also created which will contain the sheets (sheets), their names (snames) and their numbers (snumbers). All these lists will later will be appended to the out list.
If the toggle is set to True, the script starts by creating a FilteredElementCollector which is set to the variable collector. This collector filters the current document (doc) for the sheet category (BuiltInCategory.OST_Sheets) and ViewSheet class. In short, it collects all the sheets in the document.
We now have all the sheets in the collector variable so a for loop is created to check each one of those sheets. With each sheet, the sheet itself is appended to the sheets list first. Then, the sheet name is appended to the snames list by retrieving the built in parameter SHEET_NAME. Finally, the sheet number is appended to the snumbers list by retrieving the built in parameter SHEET_NUMBER.
The sheets, snames, and snumbers lists are all appended to the out list so we can retrieve each list by index from the python script output.
Here we gather all the inputs required for the Python Script nodes in parts 6 and 7. These include in the order as they appear:
Boolean : Used to turn the part 6 Python Script on or off.
Categories : Select the category to tag.
Boolean : Turn the tag leader on or off.
Number Slider : This first number slider is used to move the tag off the tagged element by a specified amount. If the number is negative, it will move to the opposite side of the element by the specified amount.
Number Slider : The second number slider allows us to locate the tag along a line based element to the specified parameter. So, for example, a parameter of 0.25 on a wall tag will locate the tag roughly 1/4 along a straight wall. This slider does not do anything if the tagged element is point based, such as a door.
Family Types : This node is used to select the family tag type. It requires the tag family type relates to the category specified.
The Python Script node used here tags the category specified, it is outlined in more detail at part 6.1. The input for index  is the filtered sheet elements obtained in part 4.
This additional Python Script allows to delete tags of a specified category. The inputs include the sheet elements, toggle to turn on and off, the category of elements tagged and a second Categories node to select the tag type which in this case is Door Tags. The details of this script can be found below at part 7.1.
As there is one script to tag and one script to delete, it is important to note that these two scripts can not be set to True at the same time. If they are, nothing will happen as you probably understand they will negate each other.
This is quite a large script so in order to keep it all in one image, I have not included the inputs in this part however, they are explained in part 5.
To begin, we start with a for loop which loops through all the views obtained in part 4. For each view, we check the ViewType and if the view type is not equal to Floor Plan, the loop continues to the next view, if it is equal, we the script continues to create a FilteredElementCollector. We do this so that only floor plans are tagged however, this could be changed to other view types if you wanted to tweak the script. The collector filters the view for elements of the category which was inputted to index  in part 6. In this case, the doors category.
With the elements collected in 6.1, we can now create another for loop to cycle through each element. We check each element to see if it is on the same level the view is cut at by getting the LevelId of the element and checking it against the built in parameter PLAN_VIEW_LEVEL of the view. If it is, the script continues and a transaction is started which allows us to make changes to the Revit model.
While we are still in the for loop for each element, the bounding box is retrieved for the element. Using the Max and Min points of the bounding box, we can create a new point (pt) at the center of the family.
With the center point of the family, we can now create a new tag using the Create.NewTag function which requires the view (view), element to tag (i), whether it requires a leader (leader set by Boolean), the type of tag (TagMode), the orientation (TagOrientation) and the tag location (pt). This function is set to the variable tag so before we execute it, we can change the tag family type using ChangeTypeId and the family selected in part 5.
Once the element is tagged we can set the tag location. First we need to determine if the element is point based or line based so we check if the location type is equal to Autodesk.Revit.DB.LocationPoint. If it is, the script continues to check the Rotation , create a new x and y with the same rotation and the distance specified in part 5 (leaderX). A new point (newp) is created with the new x and y which we use with ElementTransformUtils.MoveElement to move the tag element to the new location.
If the element to be tagged is not point based, then we check if it is line based with Autodesk.Revit.DB.LocationCurve. If it is the curve of the element is retrieved as Location.Curve and a point is extracted at the parameter set in part 5.
Next we check if the leader is turned on as set in part 5 and if so we use the location curve to move the tag. This is done by checking the Direction of the curve, calculating the CrossProduct to create a perpendicular vector which is then used to move the end points of the location curve, by the distance set in part 5 and stored in leaderX. A new line is created from these points (ln) and we find a point on that line based on the parameter used to evaluate the curve in part 6.6 (movePt). The TagHeadPosition is then moved to this point.
Finally if the the leader is not turned on , the TagHeadPosition is moved to the parameter set to point pt. The Transaction is then finished and changes are made to the model which in this case is a list of tags created.
This script starts in the same fashion as 6.1. Essentially for each view input to IN we first check if it is of view type FloorPlan and if so a FilteredElementCollector is created to find all elements of category input into IN. In this case it is the door tags type which is set to the variable tagType.
A for loop is created to cycle through each tag collected in the collector. In each loop we start a transaction and get the element that is tagged by using TaggedLocalElementId and GetElement.
Once we have the element that is tagged, we can use this information to check whether we are deleting the right tag. If the tagged elements Category.Name is equal to the category we have input to IN, the element is deleted using the Delete function. After all the tags have been cycled through, the transaction ends and the elements are deleted from the document. It isn’t vital but I have added the string “%s Tag Deleted” ele.Category.Name to the output list (lst) to show an element has been deleted. The %s formatting allows us to input a variable into the string on the fly.
With both Python Scripts in action we now have the ability to add and delete tags on the sheet views as needed. I’ve added the delete functionality as the tag elements , once created, are not stored so if the create script is re-run, new tags will be created and the old will remain. It’s important to note that this script does not allow for tagging of linked files either. The Revit API does not currently provide this ability so until it does, we can only automate tagging of local elements.
The image below shows a quick example of this script in action. I’ve ran it through lots of different situations but these things are not always full proof so please let me know if anything goes pear shaped. Also, if there is anything I haven’t fully explained or you do not fully understand please give me a shout via email or in the comments below. If you manage to evolve this script into something even better, I would love to hear about that too. If you liked the script and want more, please follow me on twitter @LearnDynamo and subscribe to the emailing list below for the next module release.