Thank you Jeremy. My solution is provided below - working but not optimised or set to handle any errors yet. It relies on Revit API 2024 for CreateMaskingRegion(). Apologies for the newbie code - it's about the best I can do so far 🙂
A quick note on why I needed this which may help others. We have to export 100's of sheets to DWG, and have all elements coloured by layer. In Revit, we often have filled regions with a solid white hatch being used to hide all sorts of things. In AutoCAD, these appear with a solid hatch and frequently mask out linework of the same layer colour. Masking regions export without creating a solid hatch, and graphically match across Revit and DWG. The aim of the script is to pick up any dimensions and linestyle changes so in Revit it looks exactly the same when converted, and doesn't cause issues in the DWG exports.
using Autodesk.Revit.Attributes;
using Autodesk.Revit.DB;
using Autodesk.Revit.UI.Selection;
using Autodesk.Revit.UI;
using System.Collections.Generic;
using System.Linq;
using Document = Autodesk.Revit.DB.Document;
using System.Collections.ObjectModel;
using System;
using Autodesk.Revit.DB.Structure;
namespace YourNameSpace
{
[TransactionAttribute(TransactionMode.Manual)]
public class ConvertRegion : IExternalCommand
{
internal UIDocument UIDoc { get; set; }
internal Document Doc { get; set; }
internal IList<CurveLoop> OldCurves { get; set; }
internal FilledRegion OldRegion { get; set; }
internal IList<LineData> OldLineData { get; set; }
internal FilledRegion MRegion { get; set;}
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
UIDoc = commandData.Application.ActiveUIDocument;
Doc = UIDoc.Document;
GetExistingCurves(); //Gets user to pick a filled region, then populates properties OldRegion and OldCurves
ICollection<ElementId> oldDims = CheckforDimensions(); //Gets any dimensions currently dependant on the region.
GetOldLineData(); //Populates OldLineData with LineData class - with X and Y start and end points of lines.
IList<LineData> projectDetailLines = GetDetailLineData(); //all the detail lines currently in the project as LineData objects with start and end coordinates
IList<LineData> matchedDetailLines = GetMatchingDetailLines(projectDetailLines); //matches any current detail lines geometrically to curves of filled region.
GetGraphicStyles(matchedDetailLines); //For matched detail lines, the graphic style is stored against the matched region curve.
//Transaction 1
CreateNewMaskingRegion(); //Run transaction to create new masking region
//The new masking regions detail lines need to be found, and the previously matched ones removed from the list.
//The graphic style stored in the old matched lines is copied across to the new matched lines.
IList<LineData> newMatchedDetailLines = GetNewDetailLines(matchedDetailLines);
//Transaction 2
UpdateGraphicsAndDims(newMatchedDetailLines, oldDims); //Apply and previous graphic styles from the filled region to the new masking region. Copy paste any dimension to new masking region.
return Result.Succeeded;
}
private void GetExistingCurves()
{
Selection selection = UIDoc.Selection;
Reference picked = selection.PickObject(ObjectType.Element);
if (picked != null)
{
OldRegion = Doc.GetElement(picked) as FilledRegion;
OldCurves = OldRegion.GetBoundaries();
}
}
private ICollection<ElementId> CheckforDimensions()
{
ICollection<ElementId> ids = new Collection<ElementId>();
if (OldRegion != null)
{
ElementClassFilter filter = new ElementClassFilter(typeof(Dimension));
IList<ElementId> elems = OldRegion.GetDependentElements(filter);
foreach (ElementId i in elems)
{
var test = Doc.GetElement(i);
if (test.Category.BuiltInCategory == BuiltInCategory.OST_Dimensions)
{
ids.Add(i);
}
}
}
return ids;
}
private void GetOldLineData()
{
OldLineData = new List<LineData>();
CurveLoopIterator itor = OldCurves.FirstOrDefault().GetCurveLoopIterator();
while (itor.MoveNext())
{
Curve curve = itor.Current;
OldLineData.Add(GetLinePoints(curve)); //populates LineData object with start and end point coords
}
}
private List<LineData> GetDetailLineData()
{
List<LineData> result = new List<LineData>();
var col = new FilteredElementCollector(Doc)
.WhereElementIsNotElementType()
.OfClass(typeof(CurveElement))
.ToElements();
foreach (Element e in col)
{
if (e is DetailLine)
{
DetailLine detailLine = (DetailLine)e;
LineData lineData = GetLinePoints(detailLine.GeometryCurve); //Call method to populate start end points of LineData object
lineData.ElId = detailLine.Id;
result.Add(lineData);
}
}
return result;
}
private LineData GetLinePoints(Curve curve)
{
LineData lineData = new LineData
(
curve.GetEndPoint(0).X,
curve.GetEndPoint(0).Y,
curve.GetEndPoint(1).X,
curve.GetEndPoint(1).Y
);
return lineData;
}
private List<LineData> GetMatchingDetailLines(IList<LineData> allDLines)
{
List<LineData> result = new List<LineData>();
foreach (LineData i in OldLineData)
{
result.AddRange(allDLines
.Where(x => x.StartX == i.StartX)
.Where(x => x.StartY == i.StartY)
.Where(x => x.EndX == i.EndX)
.Where(x => x.EndY == i.EndY)
.ToList());
}
return result;
}
private void GetGraphicStyles(IList<LineData> matchedDetailLines)
{
foreach (LineData l in matchedDetailLines)
{
DetailLine detailLine = Doc.GetElement(l.ElId) as DetailLine;
l.graphicStyle = detailLine.LineStyle as GraphicsStyle;
}
}
private void CreateNewMaskingRegion()
{
Transaction trans = new Transaction(Doc);
trans.Start("Create New Masking Region");
MRegion = FilledRegion.CreateMaskingRegion(Doc, UIDoc.ActiveView.Id, OldCurves);
trans.Commit();
}
private IList<LineData> GetNewDetailLines(IList<LineData> matchedDetailLines)
{
List<LineData> newDetailLines = GetDetailLineData(); //get all detail lines in project - including newly created ones for masking region
newDetailLines.RemoveAll(item => matchedDetailLines.Any(item2 => item.ElId == item2.ElId)); //removed previously matched lines which have the same geometry as the new ones
IList<LineData> newMatchedDetailLines = GetMatchingDetailLines(newDetailLines);
if (newMatchedDetailLines.Count == matchedDetailLines.Count)
{
for (int i = 0; i < newMatchedDetailLines.Count; i++)
{
newMatchedDetailLines[i].graphicStyle = matchedDetailLines[i].graphicStyle;
}
}
return newMatchedDetailLines;
}
private void UpdateGraphicsAndDims(IList<LineData> newMatchedDetailLines, ICollection<ElementId> oldDims)
{
Transaction trans = new Transaction(Doc);
trans.Start("Update MaskingRegion Graphics and Dims");
foreach (LineData i in newMatchedDetailLines) //Apply the graphic style to each detail line
{
DetailLine detailLine = Doc.GetElement(i.ElId) as DetailLine;
detailLine.LineStyle = i.graphicStyle;
}
ElementTransformUtils.MoveElement(Doc, MRegion.Id, XYZ.BasisZ); //apply a zero vector transform to wake up the graphics view.
ElementTransformUtils.MoveElement(Doc, MRegion.Id, XYZ.BasisZ.Negate());
UIDoc.ActiveView.HideElements(new Collection<ElementId>() { OldRegion.Id }); //Hide the filled region. If not, the copied dims may align to it.
if (oldDims.Count > 0) //apply any dimensions that were previously found
{
ElementTransformUtils.CopyElements(UIDoc.ActiveView, oldDims, UIDoc.ActiveView, null, null);
}
Doc.Delete(OldRegion.Id); //Once the dims are copied, the filled region can be deleted
trans.Commit();
}
public class LineData
{
public ElementId ElId { get; set; }
public GraphicsStyle graphicStyle { get; set; }
public double StartX { get; set; }
public double StartY { get; set; }
public double EndX { get; set; }
public double EndY { get; set; }
public LineData(double sX, double sY, double eX, double eY)
{
this.StartX = sX;
this.StartY = sY;
this.EndX = eX;
this.EndY = eY;
}
}
}
}