Below is a complete sample I've created that fits views onto a sheet.
It is run as follows
- Open an empty sheet view
- Select the views to be placed in the project browser
- Run command via very top function
This demonstrates a basic approach to adding views to a sheet you may want to also consider instances where there are many similar sized views being placed (e.g. sections drawing). Under such circumstances you may have to artificially increase the VP size being considered to make them consistent with respect to that of the largest view in order that the next view is placed along side the last view (if there is space to the right) rather than under it. Otherwise you end up with a situation where the row height is governed by the last view height and the next view, which could be slightly larger in that direction, is placed below.
The is an interesting aspect of the below sample in that when you place a viewport it is via the centre however when you estimate the rectangle size it is for the viewport and label. Therefore there is a correction vector from combined viewport and label centre back to the viewport centre in order to place the viewport.
namespace RPT_VPortTileUtil_CS
{
public class TestEntryPoints
{
//Written for Revit 2023 API
//Main entry point
public static Result TileVPTest_EntryPoint(ExternalCommandData commandData, ref string message, ElementSet elements)
{
UIApplication IntUIApp = commandData.Application;
UIDocument IntUIDoc = commandData.Application.ActiveUIDocument;
Document IntDoc = IntUIDoc.Document;
ViewSheet VSht = IntUIDoc.ActiveGraphicalView as ViewSheet;
if (VSht == null)
{
Debug.WriteLine("Active view is not a sheet view.");
return Result.Cancelled;
}
FilteredElementCollector FEC_Tb = new FilteredElementCollector(IntDoc, VSht.Id);
IList<Element> Els = FEC_Tb.OfCategory(BuiltInCategory.OST_TitleBlocks).WhereElementIsNotElementType().ToElements();
if (Els.Count != 1)
{
Debug.WriteLine("Active sheet view must have one title block instance.");
return Result.Cancelled;
}
ElementId TB_Id = Els[0].GetTypeId();
//To find outline of main drawing area for title block
//as previous thread
Outline OL = TitleBlockUtil.GetTitleBlockDrawingAreas(IntDoc)[TB_Id];
RT_Rectangle MainArea = new RT_Rectangle(OL);
List<View> ViewsList = new List<View>();
//Views selected in project browser
//If selected in main model then skip
IList<ElementId> Ids = IntUIDoc.Selection.GetElementIds() as IList<ElementId>;
List<ElementId> VPs = new List<ElementId>();
string Msg = null;
using (TransactionGroup TG = new TransactionGroup(IntDoc, "Add views to sheet"))
{
if (TG.Start() == TransactionStatus.Started)
{
using (Transaction tx = new Transaction(IntDoc, "Add viewports"))
{
if (tx.Start() == TransactionStatus.Started)
{
for (int i = 0; i <= Ids.Count - 1; i++)
{
View V = IntDoc.GetElement(Ids[i]) as View;
if (V == null)
{
//Views are either selected in project browser or other types of element are selected in model via view
break;
}
if (V.IsTemplate)
continue;
switch (V.GetPlacementOnSheetStatus())
{
case ViewPlacementOnSheetStatus.CompletelyPlaced:
//can't be placed on second sheet
continue;
case ViewPlacementOnSheetStatus.NotApplicable:
//can't be placed on sheet
continue;
}
VPs.Add(Viewport.Create(IntDoc, VSht.Id, Ids[i], XYZ.Zero).Id);
}
if (VPs.Count == 0)
{
tx.RollBack();
goto GroupEnd;
}
else
{
tx.Commit();
}
}
}
List<RT_Rectangle> VPSizes = new List<RT_Rectangle>();
foreach (ElementId item in VPs)
{
Viewport VP = IntDoc.GetElement(item) as Viewport;
VPSizes.Add(new RT_Rectangle(VP));
}
VPSizes = VPSizes.OrderBy(q => q.Area).Reverse().ToList();
RT_Rectangle[] NotAdded = null;
RT_Rectangle[] Res = RT_Rectangle.FitRectanglesInArea(new RT_Rectangle(OL), VPSizes, false, ref NotAdded);
using (Transaction tx = new Transaction(IntDoc, "Relocate and delete"))
{
if (tx.Start() == TransactionStatus.Started)
{
foreach (RT_Rectangle item in Res)
{
//Bottom left of title block
//+ Centre of view port and lable + correction from centre of viewport and label to centre of viewport
ElementTransformUtils.MoveElement(IntDoc, item.ExternalId, item.Centre + OL.MinimumPoint + item.VPCenShift);
}
List<ElementId> Del = new List<ElementId>();
foreach (RT_Rectangle item in NotAdded)
{
Del.Add(item.ExternalId);
}
if (Del.Count > 0)
{
Msg = Del.Count.ToString() + " views could not be placed in sheet allowance.";
IntDoc.Delete(Del);
}
tx.Commit();
}
}
TG.Assimilate();
goto MainEnd;
GroupEnd:
TG.RollBack();
MainEnd:;
}
}
if (string.IsNullOrEmpty(Msg) == false)
{
TaskDialog.Show("Result", Msg);
}
return Result.Succeeded;
}
}
public class RT_Rectangle
{
public ElementId ExternalId { get; set; }
private long IntId = 0;
private static long LastId = -1;
public long Id
{
get { return IntId; }
}
private void SetId()
{
LastId += 1;
IntId = LastId;
}
//Overall ords including label
private XYZ IntPosMax = null;
private XYZ IntPosMin = null;
//The allowance is sized for the overall of Viewport Outline + Label Outline
//However Viewports are placed by centre of Viewport only (not including label)
//Therefore below correction vector is stored so that VP can be placed by VP + Lavbel centre.
private XYZ IntVPShift = null;
public XYZ VPCenShift
{
get { return IntVPShift; }
}
//Only used for testing on WPF canvas
public XYZ TopLeft
{
get { return new XYZ(IntPosMin.X, IntPosMax.Y, 0); }
}
//Placement point for VP (centre of VP)
public XYZ Centre
{
get { return (IntPosMin + IntPosMax) * 0.5; }
}
public double Area
{
get { return Width * Height; }
}
public double Width
{
get { return IntPosMax.X - IntPosMin.X; }
}
public double Height
{
get { return IntPosMax.Y - IntPosMin.Y; }
}
/// <summary>
///
/// </summary>
/// <param name="R">The top left rectangle to fill gaps around</param>
/// <param name="VerticalFirst"></param>
/// <returns>Two rectangles, one below and one to side</returns>
public RT_Rectangle[] SplitAroundTopLeftRectangle(RT_Rectangle R, bool VerticalFirst, ref RT_Rectangle TL_Location)
{
double Right_SZX = Width - R.Width;
double Below_SZY = Height - R.Height;
XYZ BL0 = default(XYZ);
XYZ TL0 = default(XYZ);
XYZ BL1 = default(XYZ);
XYZ TL1 = default(XYZ);
TL_Location = new RT_Rectangle();
TL_Location.IntPosMin = this.IntPosMin + new XYZ(0, Below_SZY, 0);
TL_Location.IntPosMax = TL_Location.IntPosMin + (XYZ.BasisX * R.Width) + (XYZ.BasisY * R.Height);
//Copy VP id to returned object
TL_Location.ExternalId = R.ExternalId;
TL_Location.IntVPShift = R.IntVPShift;
if (VerticalFirst)
{
double Right_SZY = Height;
double Below_SZX = R.Width;
BL0 = this.IntPosMin + (XYZ.BasisX * R.Width);
TL0 = BL0 + (XYZ.BasisX * Right_SZX) + (XYZ.BasisY * Right_SZY);
BL1 = this.IntPosMin;
TL1 = this.IntPosMin + (XYZ.BasisX * R.Width) + (XYZ.BasisY * Below_SZY);
}
else
{
double Right_SZY = R.Height;
double Below_SZX = Width;
BL0 = this.IntPosMin + (XYZ.BasisX * R.Width) + (XYZ.BasisY * Below_SZY);
TL0 = BL0 + (XYZ.BasisX * Right_SZX) + (XYZ.BasisY * Right_SZY);
BL1 = this.IntPosMin;
TL1 = this.IntPosMin + (XYZ.BasisX * Width) + (XYZ.BasisY * Below_SZY);
}
RT_Rectangle out0 = new RT_Rectangle
{
IntPosMin = BL0,
IntPosMax = TL0
};
RT_Rectangle out1 = new RT_Rectangle
{
IntPosMin = BL1,
IntPosMax = TL1
};
return new RT_Rectangle[2] {
out0,
out1
};
}
public bool CanFitWithinOther(RT_Rectangle Other)
{
return this.Width <= Other.Width && this.Height <= Other.Height;
}
public bool CanOtherInside(RT_Rectangle Other)
{
return this.Width >= Other.Width && this.Height >= Other.Height;
}
public bool VerticalFirstReturnsLargestRectangle(RT_Rectangle R)
{
RT_Rectangle Nu = null;
RT_Rectangle[] Set0 = SplitAroundTopLeftRectangle(R, true, ref Nu);
RT_Rectangle[] Set1 = SplitAroundTopLeftRectangle(R, false, ref Nu);
double R0 = Set0.Max(j => j.Area);
double R1 = Set1.Max(j => j.Area);
return R0 >= R1;
}
private RT_Rectangle()
{
SetId();
}
public RT_Rectangle(System.Windows.Size SZ)
{
SetId();
IntPosMin = new XYZ();
IntPosMax = new XYZ(SZ.Width, SZ.Height, 0);
}
public RT_Rectangle(Outline OL)
{
SetId();
IntPosMax = OL.MaximumPoint;
IntPosMin = OL.MinimumPoint;
}
public RT_Rectangle(Viewport VP)
{
Outline OLMain = VP.GetBoxOutline();
Outline Combined = new Outline(OLMain);
dynamic OLLab = VP.GetLabelOutline();
Combined.AddPoint(OLLab.MinimumPoint);
Combined.AddPoint(OLLab.MaximumPoint);
IntVPShift = Combined.MinimumPoint - OLMain.MinimumPoint;
RT_Rectangle X = new RT_Rectangle(Combined);
IntPosMin = X.IntPosMin;
IntPosMax = X.IntPosMax;
IntId = X.IntId;
ExternalId = VP.Id;
}
//Main function
public static RT_Rectangle[] FitRectanglesInArea(RT_Rectangle StartSize, List<RT_Rectangle> RectanglesToFit, bool StopWhenCannotPlace, ref RT_Rectangle[] CouldNotFit)
{
List<RT_Rectangle> EmptyRectangles = new List<RT_Rectangle>();
EmptyRectangles.Add(StartSize);
List<RT_Rectangle> Placed = new List<RT_Rectangle>();
List<RT_Rectangle> NotPlaced = new List<RT_Rectangle>();
for (int i = 0; i <= RectanglesToFit.Count - 1; i++)
{
RT_Rectangle VP_R = RectanglesToFit[i];
List<RT_Rectangle> ResLst = EmptyRectangles.FindAll(k => k.CanOtherInside(VP_R));
RT_Rectangle Res = ResLst.Find(u => u.Area - VP_R.Area == ResLst.Min(v => v.Area - VP_R.Area));
if (Res == null)
{
if (StopWhenCannotPlace)
{
for (int k = i; k <= RectanglesToFit.Count - 1; k++)
{
NotPlaced.Add(RectanglesToFit[k]);
}
break;
}
else
{
NotPlaced.Add(VP_R);
continue;
}
}
RT_Rectangle PlacementR = null;
if (Res.VerticalFirstReturnsLargestRectangle(VP_R))
{
EmptyRectangles.AddRange(Res.SplitAroundTopLeftRectangle(VP_R, true, ref PlacementR));
}
else
{
EmptyRectangles.AddRange(Res.SplitAroundTopLeftRectangle(VP_R, false, ref PlacementR));
}
EmptyRectangles.RemoveAll(k => k.Id == Res.Id);
Placed.Add(PlacementR);
}
CouldNotFit = NotPlaced.ToArray();
return Placed.ToArray();
}
}
public static class TitleBlockUtil
{
public static Dictionary<ElementId, Outline> GetTitleBlockDrawingAreas(Document Doc)
{
FilteredElementCollector FEC = new FilteredElementCollector(Doc);
ElementCategoryFilter ECF = new ElementCategoryFilter(BuiltInCategory.OST_TitleBlocks);
List<FamilySymbol> Els = FEC.WherePasses(ECF).WhereElementIsElementType().Cast<FamilySymbol>().ToList();
Dictionary<ElementId, Outline> TBlockBoundingBoxes = new Dictionary<ElementId, Outline>();
for (int i = 0; i <= Els.Count - 1; i++)
{
FamilySymbol FS = Els[i];
Document DocT = Doc.EditFamily(FS.Family);
FilteredElementCollector FEC_Lines = new FilteredElementCollector(DocT);
ElementCategoryFilter ECF_Lines = new ElementCategoryFilter(BuiltInCategory.OST_Lines);
List<DetailLine> Lines = FEC_Lines.WherePasses(ECF_Lines).WhereElementIsNotElementType().OfType<DetailLine>().ToList();
Outline OL = null;
for (int ia = 0; ia <= Lines.Count - 1; ia++)
{
BoundingBoxXYZ BB = Lines[ia].get_BoundingBox(null);
if (OL == null)
{
OL = new Outline(BB.Min, BB.Max);
}
else
{
OL.AddPoint(BB.Min);
OL.AddPoint(BB.Max);
}
}
XYZ RoughCentre = (OL.MinimumPoint + OL.MaximumPoint) / 2;
Line XLn = Line.CreateUnbound(RoughCentre, XYZ.BasisX);
Line YLn = Line.CreateUnbound(RoughCentre, XYZ.BasisY);
double YUp = double.PositiveInfinity;
double YDown = double.NegativeInfinity;
double XLeft = double.NegativeInfinity;
double XRight = double.PositiveInfinity;
int[] BoundsSet = new int[4] { 0, 0, 0, 0 };
for (int ia = 0; ia <= Lines.Count - 1; ia++)
{
DetailLine LN = Lines[ia];
Line C = (Line) LN.GeometryCurve;
IntersectionResultArray IRA = null;
if (Math.Abs(C.Direction.DotProduct(YLn.Direction)) > 0.99)
{
SetComparisonResult SCR = XLn.Intersect(C,out IRA);
if (SCR != SetComparisonResult.Overlap)
continue;
for (int ic = 0; ic <= IRA.Size - 1; ic++)
{
IntersectionResult IR = IRA.get_Item(ic);
XYZ Pt = IR.XYZPoint;
if (Pt.X > XLeft && Pt.X < RoughCentre.X)
{
XLeft = Pt.X;
BoundsSet[0] = 1;
}
else if (Pt.X < XRight && Pt.X > RoughCentre.X)
{
XRight = Pt.X;
BoundsSet[1] = 1;
}
}
}
else if (Math.Abs(C.Direction.DotProduct(YLn.Direction)) < 0.01)
{
SetComparisonResult SCR = YLn.Intersect(C,out IRA);
if (SCR != SetComparisonResult.Overlap)
continue;
for (int ic = 0; ic <= IRA.Size - 1; ic++)
{
IntersectionResult IR = IRA.get_Item(ic);
XYZ Pt = IR.XYZPoint;
if (Pt.Y > YDown && Pt.Y < RoughCentre.Y)
{
YDown = Pt.Y;
BoundsSet[2] = 1;
}
else if (Pt.Y < YUp && Pt.Y > RoughCentre.Y)
{
YUp = Pt.Y;
BoundsSet[3] = 1;
}
}
}
}
if (!BoundsSet.Contains(0))
{
XYZ Min = new XYZ(XLeft, YDown, 0);
XYZ Max = new XYZ(XRight, YUp, 0);
TBlockBoundingBoxes.Add(FS.Id, new Outline(Min, Max));
}
DocT.Close(false);
}
return TBlockBoundingBoxes;
}
}
}