Here’s the last wish granted! RVT Links are used to model typical floors and parts are created from the walls and floors in the link instances. Here is how to use the API to set a user-created “Level” parameter for each part based on the name of the level in the host RVT nearest to the lowest point in the geometry of each part.
publicvoid setPartLevel()
{
Document doc = this.ActiveUIDocument.Document;
using (Transaction t = new Transaction(doc, "Set Part Levels"))
{
t.Start();
foreach (Part part innew FilteredElementCollector(doc).OfClass(typeof(Part)))
{
Parameter levelParam = part.Parameters.Cast<Parameter>().FirstOrDefault(q => q.Definition.Name == "Level");
if (levelParam == null)
return;
double minZ = double.PositiveInfinity;
foreach (Solid s in part.get_Geometry(new Options()))
{
foreach (Face f in s.Faces)
{
foreach (EdgeArray ea in f.EdgeLoops)
{
foreach (Edge e in ea)
{
Curve c = e.AsCurve();
if (c.GetEndPoint(0).Z < minZ)
minZ = c.GetEndPoint(0).Z;
if (c.GetEndPoint(1).Z < minZ)
minZ = c.GetEndPoint(1).Z;
}
}
}
}
Level level = new FilteredElementCollector(doc).OfClass(typeof(Level)).Cast<Level>().OrderBy(q => Math.Abs(q.Elevation - minZ)).FirstOrDefault();
levelParam.Set(level.Name);
}
t.Commit();
}
}
Joel wished for “a time stamp added to journal files. I’m thinking something like an addin or macro that just notes the current time at specified intervals. The use would be for tracking performance – what processes take a long time, and how much how much time.”
Yesterday I looked at how to purge unused families and family types. Purging materials needs a different approach because they are not used directly in the project but are referenced by other objects.
Here is an approach that uses the DocumentChanged event, rolling back Transaction Groups, and other good things do find materials that can be deleted without elements being modified. It takes a bit of time to run because it is deleting all materials and undoing the deletions that modify other elements.
publicstaticint modifiedByDeleteMaterial = 0;
publicstaticbool checkForPurgeMaterials = false;
publicstatic Document _doc = null;
publicstaticstring materialName = "";
publicvoid purgeMaterials()
{
Document doc = this.ActiveUIDocument.Document;
_doc = doc;
Application app = doc.Application;
app.DocumentChanged += documentChanged_PurgeMaterials;
List<Element> materials = new FilteredElementCollector(doc).OfClass(typeof(Material)).ToList();
string deletedMaterials = "";
int unusedMaterialCount = 0;
foreach (Element material in materials)
{
modifiedByDeleteMaterial = 0;
materialName = material.Name + " (id " + material.Id + ")";
using (TransactionGroup tg = new TransactionGroup(doc, "Delete Material: " + materialName))
{
tg.Start();
using (Transaction t = new Transaction(doc, "delete material"))
{
t.Start();
checkForPurgeMaterials = true;
doc.Delete(material.Id);
// commit the transaction to trigger the DocumentChanged event
t.Commit();
}
checkForPurgeMaterials = false;
if (modifiedByDeleteMaterial == 1)
{
unusedMaterialCount++;
deletedMaterials += materialName + Environment.NewLine;
tg.Assimilate();
}
else// rollback the transaction group to undo the deletion
tg.RollBack();
}
}
TaskDialog td = new TaskDialog("Info");
td.MainInstruction = "Deleted " + unusedMaterialCount + " materials";
td.MainContent = deletedMaterials;
td.Show();
app.DocumentChanged -= documentChanged_PurgeMaterials;
}
privatestaticvoid documentChanged_PurgeMaterials(object sender, Autodesk.Revit.DB.Events.DocumentChangedEventArgs e)
{
// do not check when rolling back the transaction groupif (!checkForPurgeMaterials)
{
return;
}
List<ElementId> deleted = e.GetDeletedElementIds().ToList();
List<ElementId> modified = e.GetModifiedElementIds().ToList();
// for debuggingstring s = "";
foreach (ElementId id in modified)
{
Element modifiedElement = _doc.GetElement(id);
s += modifiedElement.Category.Name + " " + modifiedElement.Name + " (" + id.IntegerValue + ")" + Environment.NewLine;
}
//TaskDialog.Show("d", materialName + Environment.NewLine + "Deleted = " + deleted.Count + ", Modified = " + modified.Count + Environment.NewLine + s);// how many elements were modified and deleted when this material was deleted?// if 1, then the material is unused and should be deleted
modifiedByDeleteMaterial = deleted.Count + modified.Count;
}
Building on yesterday’s post about pipe splitting, you may want a semi-intelligent way to choose quantities of pre-cut lengths to use when you break apart a long element into fabricated pieces.
Fortunately, this is the same challenge as how to make change for a given amount of money. You have a total amount (of money, length of pipe, etc) and a defined set of options (quarter, nickels, dimes, 6 and 10 foot pieces of pipe, etc) to use to get to the total. Many computer science students have probably tackled this problem and there are various solutions on the web.
The approach below uses recursion which is always exciting and adds a wrinkle that change-making doesn’t include. I added the idea of a “flexible length” and a minimum flexible length. It is possible to cut pipe into non-uniform lengths, but there is a minimum length that should not be allowed.
Some sample output for different lengths:
static List<double> amounts = new List<double>();
static List<List<double>> results = new List<List<double>>();
staticdouble minFlexibleDistance = 0;
staticdouble goal = 0;
publicvoid makeChange()
{
amounts.Clear();
// lengths that can be combined to reach the goal length
amounts.Add(4);
amounts.Add(6);
amounts.Add(10);
// this is the shortest possible length of a piece
minFlexibleDistance = 1.0;
// length of the pipe or other element to divide into pieces
goal = 26.5;
Change(new List<double>(), 0, 0, goal);
// more than one way to "make change" may have been found// prefer using no varialbe length pieces and as few pieces as possible
List<double> bestResult = new List<double>();
if (results.Count == 0)
{
TaskDialog td = new TaskDialog("Error");
td.MainInstruction = "Could not make pieces with total length = " + goal;
td.MainContent = "Minimum flexible length = " + minFlexibleDistance;
td.Show();
return;
}
List<double> resultWithShortestFlex = results.OrderBy(q => q.Last()).FirstOrDefault();
double shortestFlexPiece = resultWithShortestFlex.Last();
double numberOfPieces = Double.PositiveInfinity;
foreach (List<double> thisResult in results.Where(q => q.Last() == shortestFlexPiece))
{
if (thisResult.Count < numberOfPieces)
{
numberOfPieces = thisResult.Count;
bestResult = thisResult;
}
}
Display(bestResult);
}
privatestaticvoid Change(List<double> coins, double highest, double sum, double goal)
{
if (sum == goal) // OK - perfect match with no flexible lengths
{
coins.Add(0); // 0 is the flexible length
results.Add(coins);
return;
}
if (sum > goal)
{
double floorValue = sum - highest;
double remainder = goal - floorValue;
if (remainder >= minFlexibleDistance) // OK - can make up difference with flexible piece
{
coins.RemoveAt(coins.Count - 1);
coins.Add(remainder);
results.Add(coins);
return;
}
else// BAD - min flexible distance would be too small - do not use this set of values
{
return;
}
}
foreach (double value in amounts)
{
if (value >= highest) // Only add higher or equal amounts.
{
List<double> copy = new List<double>(coins);
copy.Add(value);
Change(copy, value, sum + value, goal);
}
}
return;
}
privatestaticvoid Display(List<double> coins)
{
string data = "";
foreach (double amount in amounts)
{
double count = coins.Count(value => value == amount);
string pieces = "pieces";
if (count == 1)
pieces = "piece";
data += count + " " + pieces + " each " + amount + " feet long" + Environment.NewLine;
}
foreach (double coin in coins)
{
if (!amounts.Contains(coin))
{
data += "One variable length piece = " + coin + " feet long" + Environment.NewLine;
}
}
TaskDialog td = new TaskDialog("Output");
td.MainInstruction = "Best way to make " + goal.ToString() + " feet from pieces";
td.MainContent = data;
td.Show();
}
At the #RTCEUR “All you ever wanted to ask about the Revit API” panel, Sasha Crotty strongly recommended submitting your suggestions for making the API better to the Revit Ideas board. This is a place where other people can vote for requests and Autodesk managers and developers are paying attention to the most popular items.
Even if you have already made requests on Autodesk’s Revit API Forum, if you really want the change/fix to be made Autodesk would like to submit an “idea” too.
Brian wished for “splitting MEP Pipework at certain distances on the horizontals with Revit MEP Hangers”. Here’s a look at how the API can be used to create a new set of pipes of a specified length to replace one long pipe. Hangers are placed at the start and end of each new pipe.
One opportunity for improvement would be to do something more sophisticated at the end of the original pipe. If your pipe comes in standard lengths, you may not want to end up with a 1′ piece of pipe at the end if you need 37′ total feet of pipe and your standard length is 6′. If that sounds like an interesting problem to solve, send me a tweet and maybe I’ll post the solution tomorrow. But now its time to find out more about the PORT in Porto.
publicvoid SplitPipe()
{
UIDocument uidoc = this.ActiveUIDocument;
Document doc = uidoc.Document;
Pipe pipe = doc.GetElement(uidoc.Selection.PickObject(ObjectType.Element)) as Pipe;
ElementId levelId = pipe.get_Parameter(BuiltInParameter.RBS_START_LEVEL_PARAM).AsElementId();
Line pipeLine = ((LocationCurve)pipe.Location).Curve as Line;
double segmentLength = 6;
List<Line> newLines = new List<Line>();
double i = 0;
for (i=0; i <= pipeLine.Length - segmentLength; i = i + segmentLength)
{
XYZ end0 = pipeLine.Evaluate(i, false);
XYZ end1 = pipeLine.Evaluate(i + segmentLength, false);
newLines.Add(Line.CreateBound(end0, end1));
}
newLines.Add(Line.CreateBound(pipeLine.Evaluate(i, false), pipeLine.Evaluate(1, true)));
FamilySymbol hangerSymbol = new FilteredElementCollector(doc)
.OfClass(typeof(FamilySymbol))
.Cast<FamilySymbol>()
.FirstOrDefault(q => q.Family.Name == "Hanger" &&
q.Family.FamilyCategory.Id.IntegerValue == (int)BuiltInCategory.OST_PipeAccessory);
using (Transaction t = new Transaction(doc, "Split Pipe"))
{
t.Start();
View3D view3d = View3D.CreateIsometric(doc, new FilteredElementCollector(doc)
.OfClass(typeof(ViewFamilyType))
.Cast<ViewFamilyType>()
.FirstOrDefault(q => q.ViewFamily == ViewFamily.ThreeDimensional).Id);
List<Tuple<FamilyInstance, double>> hangers = new List<Tuple<FamilyInstance, double>>();
for (int lineCtr = 0; lineCtr < newLines.Count; lineCtr++)
{
Pipe newPipe = Pipe.Create(doc, pipe.MEPSystem.GetTypeId(), pipe.PipeType.Id, levelId, newLines[lineCtr].GetEndPoint(0), newLines[lineCtr].GetEndPoint(1));
ReferenceIntersector ri = new ReferenceIntersector(new ElementClassFilter(typeof(Ceiling)), FindReferenceTarget.Face, view3d);
ReferenceWithContext rwc = ri.FindNearest(newLines[lineCtr].GetEndPoint(1), XYZ.BasisZ);
if (rwc != null)
{
Reference r = rwc.GetReference();
Ceiling ceiling = doc.GetElement(r) as Ceiling;
Options opt = new Options();
opt.ComputeReferences = true;
PlanarFace ceilingBottomFace = null;
Solid s = ceiling.get_Geometry(opt).Cast<GeometryObject>().FirstOrDefault(q => q is Solid) as Solid;
foreach (Face face in s.Faces)
{
if (face is PlanarFace)
{
PlanarFace pf = face as PlanarFace;
if (pf.FaceNormal.IsAlmostEqualTo(XYZ.BasisZ.Negate()))
{
ceilingBottomFace = pf;
break;
}
}
}
if (ceilingBottomFace != null)
{
FamilyInstance fiHanger = doc.Create.NewFamilyInstance(ceilingBottomFace, newLines[lineCtr].GetEndPoint(1), pipeLine.Direction.CrossProduct(XYZ.BasisZ), hangerSymbol);
hangers.Add(new Tuple<FamilyInstance, double>(fiHanger, rwc.Proximity));
if (lineCtr == 0)
{
fiHanger = doc.Create.NewFamilyInstance(ceilingBottomFace, newLines[lineCtr].GetEndPoint(0), pipeLine.Direction.CrossProduct(XYZ.BasisZ), hangerSymbol);
hangers.Add(new Tuple<FamilyInstance, double>(fiHanger, rwc.Proximity));
}
}
}
}
foreach (Tuple<FamilyInstance, double> tup in hangers)
{
tup.Item1.Parameters.Cast<Parameter>().FirstOrDefault(q => q.Definition.Name == "Distance from Ceiling to Pipe Center").Set(tup.Item2);
}
doc.Delete(pipe.Id);
doc.Delete(view3d.Id);
t.Commit();
}
}
The first wish granted from Porto is for Phil who wished for “a solution to purging unused Revit elements from a Revit model”. Different elements need to be purged in different ways. This code will clean up unused system and loadable families and their types.
How are you wishing to make Revit better?
The “RollbackIfErrorOccurs” section is used to handle the case where deleting a wall type would result in this error.
publicvoid purgeFamiliesAndTypes()
{
Document doc = this.ActiveUIDocument.Document;
// List of categories whose families will be purged
List<int> categoriesToPurge = new List<int>();
categoriesToPurge.Add((int)BuiltInCategory.OST_StructuralFraming);
categoriesToPurge.Add((int)BuiltInCategory.OST_Walls);
List<ElementId> typesToDelete = new List<ElementId>();
// Check all element types whose category is contained in the category list foreach (ElementType et innew FilteredElementCollector(doc)
.OfClass(typeof(ElementType))
.Cast<ElementType>()
.Where(q => q.Category != null &&
categoriesToPurge.Contains(q.Category.Id.IntegerValue)))
{
// if there are no elements with this type, add it to the list for deletionif (new FilteredElementCollector(doc)
.WhereElementIsNotElementType()
.Where(q => q.GetTypeId() == et.Id).Count() == 0)
{
typesToDelete.Add(et.Id);
}
}
using (TransactionGroup tg = new TransactionGroup(doc, "Purge families"))
{
tg.Start();
foreach (ElementId id in typesToDelete)
{
using (Transaction t = new Transaction(doc, "delete type"))
{
// Do not delete type if it would result in error such as// "Last type in system family "Stacked Wall" cannot be deleted."
FailureHandlingOptions failOpt = t.GetFailureHandlingOptions();
failOpt.SetClearAfterRollback(true);
failOpt.SetFailuresPreprocessor(new RollbackIfErrorOccurs());
t.SetFailureHandlingOptions(failOpt);
t.Start();
try
{
doc.Delete(id);
}
catch
{}
t.Commit();
}
}
// Delete families that now have no types
IList<ElementId> familiesToDelete = new List<ElementId>();
foreach (Family family innew FilteredElementCollector(doc)
.OfClass(typeof(Family))
.Cast<Family>()
.Where(q => categoriesToPurge.Contains(q.FamilyCategory.Id.IntegerValue)))
{
// add family to list if there are no instances of any type of this familyif (new FilteredElementCollector(doc)
.OfClass(typeof(FamilyInstance))
.Cast<FamilyInstance>()
.Where(q => q.Symbol.Family.Id == family.Id)
.Count() == 0)
{
familiesToDelete.Add(family.Id);
}
}
using (Transaction t = new Transaction(doc, "delete families with no types"))
{
t.Start();
doc.Delete(familiesToDelete);
t.Commit();
}
tg.Assimilate();
}
}
publicclass RollbackIfErrorOccurs : IFailuresPreprocessor
{
public FailureProcessingResult PreprocessFailures(FailuresAccessor failuresAccessor)
{
// if there are any failures, rollback the transactionif (failuresAccessor.GetFailureMessages().Count > 0)
{
return FailureProcessingResult.ProceedWithRollBack;
}
else
{
return FailureProcessingResult.Continue;
}
}
}
Start thinking about now about how you’d like to Make Revit Better and post them as comments here or tweet them to @BoostYourBIM. Wishes will be granted live from Porto starting on Thursday morning right up until Saturday’s gala dinner.