While I am binge blogging (2 in a day – which is more than I have done all year, so yes…this is a binge!), I thought I would write about another blast from the past… Place Multiple Views on Multiple Sheets. This was something I did for the forum a while ago and was quite popular and apparently still is. Originally it started off as just a Legend Copier, then someone wanted schedules, then something other and it kinda evolved into this and will probably still evolve further. However, there was and still is some confusion about how it works… as a coder, it is all too easy to forget that not everyone thinks as you do and documentation can sometimes seem pointless if you (the coder) thinks it is too simple a script to document… this is totally wrong, I have learnt over the past few years at work that documentation is super critical, even for the most simple of scripts… that said, I’m still pretty useless at getting around to documentation! 😛
For the lazy readers, I have made you a Table of Contents… You’re welcome!
Table Of Contents…
- Node Code
- Examples
Node Code…
Firstly, I will cover the code for some of the key nodes in these workflows, then I will cover some examples of how to use them.
View.ViewTypes
Gets all the Built-In ViewTypes in Revit.

"""
Description: Gets the Built-In ViewTypes.
Author: Dan Woodcock
Website: https://danimosite.wordpress.com
Licence: N/A
"""
###### Imports ######
import clr
clr.AddReference("RevitServices")
import RevitServices
from RevitServices.Persistence import DocumentManager
doc = DocumentManager.Instance.CurrentDBDocument
clr.AddReference("RevitAPI")
from Autodesk.Revit.DB import *
import System
###### Main Script ######
# Get and return all Built-In ViewTypes to the User...
OUT = System.Enum.GetValues(ViewType)
Sheet.GetViewsAndLocations
This node will report the Position and associated Views to all the Viewports on the the given Sheet. It takes and optional ViewTypeFilter IN-Port, this allows you to filter which ViewPorts are operated on. Use the View.ViewTypes node above to get all the Built-In ViewTypes in Revit and plug some of these in. If no ViewTypes given then all ViewPorts will be operated on.

"""
Description: Gets the Viewports location and respective underlying Views for all the Viewports on the sheet. Optional input is for filtering by the ViewType i.e FloorPlans, Legends, threeDView etc
Author: Dan Woodcock
Website: https://danimosite.wordpress.com
Licence: N/A
"""
###### Import References ######
import clr
clr.AddReference("ProtoGeometry")
from Autodesk.DesignScript import Geometry as geom
clr.AddReference("RevitServices")
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument
clr.AddReference("RevitNodes")
import Revit
clr.ImportExtensions(Revit.Elements)
clr.ImportExtensions(Revit.GeometryConversion)
clr.AddReference("RevitAPI")
from Autodesk.Revit.DB import *
import System
###### Definitions ######
def tolist(obj1):
if hasattr(obj1,"__iter__"): return obj1
else: return [obj1]
# Gets the Centre of the ScheduleSheetInstance
def GetScheduleCentre(v):
# Get ScheduleSheetInstance's bounding box...
bBox = v.get_BoundingBox(doc.GetElement(v.OwnerViewId)).ToProtoType()
# Return the centre of the BoindingBox as DS Point...
return geom.Point.ByCoordinates((bBox.MinPoint.X+bBox.MaxPoint.X)/2,(bBox.MinPoint.Y+bBox.MaxPoint.Y)/2,0)
###### Inputs ######
sheets = tolist(UnwrapElement(IN[0])) # The sheets to operate on...
# ViewTypes to filter ViewPorts with. If none given then all ViewTypes will be used...
if IN[1] == None:
filter = System.Enum.GetValues(ViewType)
else:
filter = tolist(UnwrapElement(IN[1]))
###### Outputs ######
viewsOut = []
locsOut = []
###### Main Script ######
# Loop through all given sheets...
for sht in sheets:
vArr = []
locArr = []
# First get all the ViewPorts on sheet...
vPorts = sht.GetAllViewports()
# If there are ViewPorts on the Sheet...
if vPorts:
# Get the View. ViewPorts and Views are different Elements. The ViewPort is sort of like a container for Views so we need to get the View from the ViewPort.
vPorts = [doc.GetElement(vp) for vp in vPorts]
for vp in vPorts:
# Get and return the View...
v = doc.GetElement(vp.ViewId)
if v.ViewType in filter:
vArr.append(v)
# Get and return the Centre of the ViewPorts locations....
locArr.append(vp.GetBoxCenter().ToPoint())
# Next we get the schedules. These are not classified as ViewPorts so they will not be found in the above method. Instead we can use the FilteredElementCollector to find all the ScheduleSheetInstance's on the sheet.
schedules = [s for s in FilteredElementCollector(doc,sht.Id).OfClass(ScheduleSheetInstance).ToElements() if not s.IsTitleblockRevisionSchedule]
# If there are Schedules on the Sheet...
if schedules:
for s in schedules:
vs = doc.GetElement(s.ScheduleId)
if vs.ViewType in filter:
# Get and return the Schedule. Like ViewPorts and Views, the ScheduleSheetInstance is like a container for the Schedule on a sheet. We can get to this by getting the associated Schedule with this ScheduleSheetInstance...
vArr.append(vs)
# Get and return the centre of the Schedule. Typically the schedule is placed by the Upper Left corner, in this case we want the Centre so we are using a custom definition to do this.
locArr.append(GetScheduleCentre(s))
# Return the Views on this Sheet...
viewsOut.append(vArr)
# Return the locations of the Views on this Sheet...
locsOut.append(locArr)
# Return the results to the user...
OUT = viewsOut, locsOut
Sheet.PreviewViewsOnSheet
Used with Sheet.PlaceViews as a way of previewing the location of Views on Sheet before you place them. It will draw the Sheet and View perimeters in Dynamo for you to visualize placement.


"""
Description: Used with Sheet.PlaceViews as a way of previewing the location if Views on Sheet before you place them. It will draw the Sheet and View perimeters in Dynamo for you to visualise placement.
NOTE: Data Structure of inputs is important. Sheets must be a flat list (no nesting), the Views and Locations must be nested lists where 1st levels matches the number of sheets and second level should match each other.
So...
Sheets = [X]
Views = [X][Y]
Locations = [X][Y]
Author: Dan Woodcock
Website: https://danimosite.wordpress.com
Licence: N/A
"""
################### Import References ###################
import clr
clr.AddReference("ProtoGeometry")
from Autodesk.DesignScript import Geometry as geom
clr.AddReference("RevitServices")
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument
app = DocumentManager.Instance.CurrentUIApplication.Application
clr.AddReference("RevitNodes")
import Revit
clr.ImportExtensions(Revit.Elements)
clr.ImportExtensions(Revit.GeometryConversion)
clr.AddReference("RevitAPI")
from Autodesk.Revit.DB import *
import System
from System.Collections.Generic import *
################### Definitions ###################
def tolist(obj1):
if hasattr(obj1,"__iter__"): return obj1
else: return [obj1]
def getOutlines(v):
w = 0
l = 0
if not v.ViewType == ViewType.Schedule:
#get outline
oLine = v.Outline
minU = oLine.Min.U*304.8
minV = oLine.Min.V*304.8
maxU = oLine.Max.U*304.8
maxV = oLine.Max.V*304.8
#get width and length of Rectangle
w = maxU-minU
l = maxV-minV
else:
td = v.GetTableData()
w = td.Width * 304.8
i = 0
while i < td.NumberOfSections:
tsd = td.GetSectionData(i)
j = 0
while j < tsd.NumberOfRows:
l = l + tsd.GetRowHeight(j)*304.5
j = j+1
i = i+1
# rough scaling as exact dimension is not accessible via API
l = l*1.35
#create rectangle
myRec = geom.Rectangle.ByWidthLength(w,l)
#move rectangle so bottom left corner is at origin
myRec2 = geom.Geometry.Translate(myRec,w/2,l/2,0)
#add rectangle to output list
return myRec2,w,l
def moveRec(rec,p):
cntr = geom.Polygon.Center(rec)
vec = geom.Vector.ByTwoPoints(cntr,p)
return geom.Geometry.Translate(rec,vec)
def setTiling(xd,yd,rec):
tileVec = geom.Vector.ByCoordinates(xd,yd,0)
#cntr = geom.Polygon.Center(rec)
#vec = geom.Vector.ByTwoPoints(cntr,tileLoc)
return geom.Geometry.Translate(rec,tileVec)
################### Input Variables ###################
#get the sheet in port IN[1] and UnwrapElement if not already...
sheetsIN = tolist(UnwrapElement(IN[0]))
viewsIN = tolist(UnwrapElement(IN[1]))
locsIN = tolist(UnwrapElement(IN[2]))
################### Output Variables ###################
outList = []
################### other Variables ###################
#conversion between ft and mm
ft2mm = 304.8
xDiff = 0
yDiff = 0
yMax = 0
cx = 0
cy = 0
spacing = 100
################### Script ###################
if len(sheetsIN) == len(viewsIN) == len(locsIN):
for sht,views,locs in zip(sheetsIN,viewsIN,locsIN):
arr = []
# get sheet outline info...
sgo = getOutlines(sht)
# sheet outline...
sOLine = sgo[0]
arr.append(setTiling(xDiff,yDiff,sOLine))
if len(views) == len(locs):
for v,l in zip(views,locs):
vOLine = moveRec(getOutlines(v)[0],l)
arr.append(setTiling(xDiff,yDiff,vOLine))
outList.append(arr)
cx = cx+1
else:
outList.append("Number of views does not match number of locations")
# Tiling logic...
# if number of sheets along x axis > 5 then return to a new line above by the largest sheet height...
if cx >= 5:
cx = 0
xDiff = 0
yDiff = yDiff + yMax + spacing
cy = cy+1
# else if number of sheets along x axis <= 5 then store the largest sheet height found and move a long the x axis...
else:
xDiff = xDiff + sgo[1] + spacing
if sgo[2] > yMax: yMax = sgo[2]
OUT = outList
else:
OUT = "Number of sheets does not match either the number of view lists of location lists"
Sheet.PlaceViews
This node attempts to place multiple views on sheet.
NOTE: This is the bit people get confused about. The Data Structure of the inputs is of utmost importance. Sheets must be a flat list (no nesting), the Views and Locations must be nested lists where outer list length matches the number of sheets and inner lists count should match each other. See below image for how the Data Structure should look like…

See how I am giving the node 7 Sheets in a flat list (no nesting), then for the Views and Location inputs I am giving a list of lists where I have 7 lists of 3 views and 7 lists of 3 locations. So this is a list of views and a list of locations to place the views per sheet! It is critical to keep this structure as it is hard coded.
"""
Description: Tries to place multiple views on sheet.
NOTE: Data Structure of inputs is important. Sheets must be a flat list (no nesting), the Views and Locations must be nested lists where 1st levels matches the number of sheets and second level should match each other.
So...
Sheets = [X]
Views = [X][Y]
Locations = [X][Y]
Author: Dan Woodcock
Website: https://danimosite.wordpress.com
Licence: N/A
"""
################### Import References ###################
import clr
clr.AddReference("ProtoGeometry")
from Autodesk.DesignScript import Geometry as geom
clr.AddReference("RevitServices")
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument
app = DocumentManager.Instance.CurrentUIApplication.Application
clr.AddReference("RevitNodes")
import Revit
clr.ImportExtensions(Revit.Elements)
clr.ImportExtensions(Revit.GeometryConversion)
clr.AddReference("RevitAPI")
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Electrical import PanelScheduleSheetInstance as pssi
###### Definitions ######
# Ensure object is a list of objects.
def tolist(obj1):
if hasattr(obj1,"__iter__"): return obj1
else: return [obj1]
# Gets the Centre of the ScheduleSheetInstance
def GetScheduleCentre(v):
bBox = v.get_BoundingBox(doc.GetElement(v.OwnerViewId)).ToProtoType()
return geom.Point.ByCoordinates((bBox.MinPoint.X+bBox.MaxPoint.X)/2,(bBox.MinPoint.Y+bBox.MaxPoint.Y)/2,0)
# Moves the Schedule post placement so that the centre is aligned to the placement point.
def MoveSchedule(v):
bBox = v.get_BoundingBox(doc.GetElement(v.OwnerViewId))
w = (bBox.Max.X - bBox.Min.X)
l = (bBox.Max.Y - bBox.Min.Y)
tVec = XYZ(-w/2,l/2,0)
try:
ElementTransformUtils.MoveElement(doc,v.Id,tVec)
return True
except:
return False
# Tries to get the Viewport of the given a View within the given Sheet to search in.
def FindViewportByView(sht,v):
# Handle Schedules...
# If View is schedule or panel schedule...
if v.ViewType == ViewType.Schedule or v.ViewType == ViewType.PanelSchedule:
# Get first ViewPort where View Ids match...
return next((s for s in FilteredElementCollector(doc,sht.Id).OfClass(ScheduleSheetInstance).ToElements() if s.ScheduleId == v.Id),None)
# Handle ViewPorts...
vPorts = sht.GetAllViewports()
if vPorts:
# If there are Viewport, get the ViewPort Element from ViewPort Id...
vPorts = [doc.GetElement(vp) for vp in vPorts]
# Get first ViewPort where View Ids match...
return next((vp for vp in vPorts if vp.ViewId == v.Id),None)
# Correct position post ViewPort placement. When placing views on sheet, there is a slight dicrepancy (a known Revit API issue) where placement is not exactly at the coordinates we want.
def CorrectPlacement(sht,views,locs):
for v,loc in zip(views,locs):
# Find the ViewPort on sheet by View...
vp = FindViewportByView(sht,v)
# If the ViewPort has been found...
if vp:
cPt = loc
# Handle Schedules...
if v.ViewType == ViewType.Schedule:
cPt = GetScheduleCentre(vp)
# Handle Panel Schedule...
elif v.ViewType == ViewType.PanelSchedule:
cPt = vp.Origin.ToPoint()
# Handle everything else (Note: Additional handling may be required, further testing needed here)...
else:
cPt = vp.GetBoxCenter().ToPoint()
# If distance is greater than some small value we should correct placement...
if cPt.DistanceTo(loc) > 0.01:
# For some reason, conversion ToXyx() doesn't concvert to internal unit type, so we apply scaling from mm to ft...
d = geom.Vector.Scale(geom.Vector.ByTwoPoints(cPt,loc),0.00328084).ToXyz()
# Move the Viewport...
ElementTransformUtils.MoveElement(doc,vp.Id,d)
# Try add views to sheet given a sheet, a list of Views and a matching list of points.
def AddViewToSheet(sht,views,locs):
arr = []
for v,l in zip(views, locs):
# By default we will assume all views can be added to sheet until we test if they actually can be...
canAddToSht = True;
# Test if View can be added to sheet...
if v.ViewType == ViewType.Schedule or v.ViewType == ViewType.PanelSchedule:
# Schedules can be added more that one to a sheet, here we check if the Schedule is already on the sheet...
canAddToSht = CanPlaceScheduleOnSheet(sht,v)
else:
# If the View is not a Schedule we can use the BuiltIn revit Method...
canAddToSht = Viewport.CanAddViewToSheet(doc, sht.Id, v.Id)
# If we can add the view to the Sheet...
if canAddToSht:
try:
# If the view is a schedule...
if v.ViewType == ViewType.Schedule:
vp = ScheduleSheetInstance.Create(doc, sht.Id, v.Id, l.ToXyz())
# Schedules are placed at top left corner, here we correct to centre placement...
MoveSchedule(vp)
arr.append(vp)
# If the View is a Panel Schedule...
elif v.ViewType == ViewType.PanelSchedule:
vp = pssi.Create(doc, v.Id, sht)
# Set the origin (centre) of the Panel Schedule...
vp.Origin = l.ToXyz()
arr.append(vp)
# If the View is something other...
else:
vp = Viewport.Create(doc, sht.Id, v.Id, l.ToXyz())
arr.append(vp)
except Exception, e:
arr.append(str(e))
else:
arr.append("Can't add this view to sheet either because the view is already on sheet or because there was an unknown error")
return arr
# Test if the Schedule is already on sheet.
def CanPlaceScheduleOnSheet(sht,sch):
# Get all schedules on sheet that are not the TitleblockRevisionSchedule Type...
schedules = [s for s in FilteredElementCollector(doc,sht.Id).OfClass(ScheduleSheetInstance).ToElements() if not s.IsTitleblockRevisionSchedule]
# If the sheet has Schedules...
if schedules:
for s in schedules:
# Is there a schedule on sheet that is the same as the one given...
if s.ScheduleId.IntegerValue == sch.Id.IntegerValue:
# Return false, that the schedule should not be placed on sheet...
return False
# Return true, the default case...
return True
###### Inputs ######
run = tolist(IN[0])[0] # Should this node be run...
sheetsIN = tolist(UnwrapElement(IN[1])) # The Sheets to add the views to, this should be a flat list. So NO lists of lists...
viewsIN = tolist(UnwrapElement(IN[2])) # The Views to add to the sheet. This should be in the list structure [][]...
locsIN = tolist(IN[3]) # List of locations as DS Points. This should have the same list structure as the viewsIN, so [][]...
###### Outputs ######
outList = []
###### Script ######
if run:
# check if list lengths are the same (the views and locations will be lists of lists here, but the outermost list layer should all be the same length)
if len(sheetsIN) == len(viewsIN) == len(locsIN):
# Open a Transaction to create ViewPorts...
TransactionManager.Instance.EnsureInTransaction(doc)
# loop through all lists (views and locs will be lists of lists)
for sht,views,locs in zip(sheetsIN,viewsIN,locsIN):
# test if number of views and number of locations match
if len(views) == len(locs):
outList.append(AddViewToSheet(sht,views,locs))
else:
outList.append("Number of views does not match number of locations")
TransactionManager.Instance.TransactionTaskDone()
# Here we will correct the dicrepancy between desired location and placed ViewPorts actual location...
# Regenerate the document so we can get ViewPorts actual location...
doc.Regenerate()
# Open a new Transaction to make corrections...
TransactionManager.Instance.EnsureInTransaction(doc)
for sht,views,locs in zip(sheetsIN,viewsIN,locsIN):
try:
# Try correct placement discrepancy...
CorrectPlacement(sht,views,locs)
except Exception,ex:
pass
TransactionManager.Instance.TransactionTaskDone()
# Return results to user...
OUT = sheetsIN, outList
else:
OUT = "Number of sheets does not match either the number of view lists of location lists","Number of sheets does not match either the number of view lists of location lists"
else:
OUT = "Set Run to True","Set Run to True"
Examples…
Legend/Schedule Copier
This example uses the nodes above make a Legend/Schedule Copier. Select a sheet you want to copy the Legends and Schedules from, then choose which sheets you want to copy to, then select which ViewTypes you want to copy (Legends/Schedules) and the script will take care of the rest. This uses the awesome Data|Shapes Package for the User Interface (if you don’t have this package installed then you really must install it, not only for this graph to run, but also because it is a bad-ass package).
I will hopefully make some more examples when I have a bit of time, I will probably combine the ViewsByPhase scripts to make a Construction Sequence Sheet Generator which has been on my list for about 2 years ! : D
Anyway, for now I hope you find this useful and as always… Thanks for reading! 🙂
Leave a Reply