Hello. I just started to understand API Revit, so I need your help.
How to move selected elements to another level while maintaining their position in space?
Thank you in advance for your assistance!
Solved! Go to Solution.
Link copied
Hello. I just started to understand API Revit, so I need your help.
How to move selected elements to another level while maintaining their position in space?
Thank you in advance for your assistance!
Solved! Go to Solution.
Welcome to the Revit API. Can you achieve what you want manually in the end user interface? If so, that is a good start. If not, it would be good to check that first, determine the optimal workflow and best practices. Here is the standard approach to address a Revit API programming task:
Possibly, some elements cannot simply be moved to an different level, but need to be recreated from scratch based on the new level.
There is an older post showing how to do something similar here: https://forums.autodesk.com/t5/revit-api-forum/change-the-level-of-an-element/td-p/3707640
Here is an example command that will change the level of the selected elements. Note that you will need to determine which parameter you want to change for different types of elements, and as @jeremy_tammik noted, you may not be able to change the level of some elements. The example below is changing the level for selected piping elements.
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
var doc = commandData.Application.ActiveUIDocument.Document;
var selectedIds = commandData.Application.ActiveUIDocument.Selection.GetElementIds();
var selectedElements = selectedIds.Select(x => doc.GetElement(x)).ToList();
var newLevelName = "L2";
var newLevel = new FilteredElementCollector(doc)
.OfClass(typeof(Level))
.FirstOrDefault(x => x.Name == newLevelName) as Level;
var levelHostedElements = selectedElements
.Where(x => x.LevelId != null && x.LevelId != ElementId.InvalidElementId)
.ToList();
using (var t = new Transaction(doc, "update level"))
{
t.Start();
foreach (var element in levelHostedElements)
{
// NOTE: you will need to select the correct parameter for the element type you are targeting
var levelParameter = element.get_Parameter(BuiltInParameter.RBS_START_LEVEL_PARAM);
if (levelParameter?.HasValue == true /*&& offsetParameter?.HasValue == true*/)
{
var oldLevel = doc.GetElement(levelParameter.AsElementId()) as Level;
levelParameter.Set(newLevel.Id);
}
}
t.Commit();
}
return Result.Succeeded;
}
Thanks for the answer, although this is not exactly what I wanted. I wanted to select a level (in dialog window) and transfer all elements from it to the transit level. Elements that were not transferred - display in the dialog box
I believe this approach would work, you would just need some handling for parameter/element type match up. This seems like a perfect match for an abstract factory pattern or something similar. You might also save some time using the Revit Lookup tools to identify which parameters match to which types.
It's not ideal, but I do not think that there is a universal solution to changing the level of an element. As far as I understand, the reason for this is that different elements are hosted by and associated with levels in different ways. So Revit's engine under the hood is doing different things to make that work, and the options we have exposed to us in the API therefore differ by type. @jeremy_tammik is the expert in that regard, but this is what I understand to be the case.
Regarding handling moving everything on a given level, that can be accomplished with some changes to my example. Where I have hard-code the level name var newLevelName = "L2"; you could easily replace that with a UI allowing users to select the destination level. Similarly, you could add a UI to allow the user to select a source level, and supply that id to this block of code:
var levelHostedElements = selectedElements
.Where(x => x.LevelId == sourceLevel.Id)
.ToList();
Do you also need help with selection and UI components?
For now I have implemented the following logic
public partial class FormChageLevel : Window
{
private Document _doc;
public Dictionary<string, List<Element>> ElementsByLevel { get; set; }
public string[] LevelNames { get; set; }
public FormChageLevel(Document document)
{
_doc = document;
InitializeComponent();
ElementsByLevel = GetElementsByLevel(_doc);
this.DataContext = this;
}
// Formation of a Dictionary to display the number of elements per level
// In the future, this method will be necessary to display information on the user interface.
public Dictionary<string, List<Element>> GetElementsByLevel(Document doc)
{
Dictionary<string, List<Element>> elementsByLevel = new Dictionary<string, List<Element>>();
FilteredElementCollector collector = new FilteredElementCollector(doc);
List<Level> levels = collector.OfClass(typeof(Level)).Cast<Level>().ToList();
foreach (Level level in levels)
{
List<Element> elementNames = new List<Element>();
ElementLevelFilter levelFilter = new ElementLevelFilter(level.Id);
FilteredElementCollector elementsList = new FilteredElementCollector(doc);
List<Element> elements = elementsList.WherePasses(levelFilter).ToList();
foreach (Element element in elements)
{
elementNames.Add(element);
}
elementsByLevel.Add(level.Name, elementNames);
}
return elementsByLevel;
}
// Interface event handling method
private void LevelExport_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
string selectedKey = LevelExport.SelectedItem as string;
if (selectedKey != null && ElementsByLevel.ContainsKey(selectedKey))
{
List<Element> elements = ElementsByLevel[selectedKey];
LevelExportElementList.Text = "";
foreach (Element element in elements)
{
LevelExportElementList.Text += $"ID: {element.Id}; NAME: {element.Name}\n";
}
}
}
// Interface event handling method
private void LevelImport_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
string selectedKey = levelImport.SelectedItem as string;
if (selectedKey != null && ElementsByLevel.ContainsKey(selectedKey))
{
List<Element> elements = ElementsByLevel[selectedKey];
LevelImportElementList.Text = "";
foreach (Element element in elements)
{
LevelImportElementList.Text += $"ID: {element.Id}; NAME: {element.Name}\n";
}
}
}
// Moving elements to another level while maintaining their position in space
// It is assumed that the new level was created by the user independently in advance
public void TransferringElementsToAnotherLevel()
{
string exportLevelName = LevelExport.SelectedItem as string;
string importLevelName = levelImport.SelectedItem as string;
foreach (Element element in ElementsByLevel[exportLevelName])
{
// I don't really understand the logic behind the additional parameters.
}
}
// Close Window
private void Button_CloseClick(object sender, RoutedEventArgs e)
{
this.Close();
}
// Button for TransferringElementsToAnotherLevel()
private void Button_ClickTransferringElements(object sender, RoutedEventArgs e)
{
TransferringElementsToAnotherLevel();
}
}
}
At the moment I don't quite understand the logic behind the additional parameters. I want to implement moving objects to a new level while maintaining the position of these objects in space. It is assumed that the new level was created by the user independently in advance. Objects that fail to move should remain at the level they were originally at.
Thank you for the nice sample to change the level of pipes. I shared it on the blog for future reference:
Last December I published a tool which do just that. Leveler - you can find it on Autodesk App Store. It helps reassign levels, plus have some other features for grids and levels. (https://w7k.pl/tools/leveler/)
Please find below the video about it. I explained the problem fairly deeply while presenting the tool.
https://youtu.be/cXdXycnS-VA?si=bfwPf1eusCXo_CUq
In short, revit objects have different parameters for levels and offsets from them. Also there is the whole category of object where assignment to levels is a "mess" (face based families, floor based etc). Please have a look at 9 minute of my video, where in 3 minutes I'm showing some of the problems -10:55 is the best. For example, changing levels of structural foundations piles does nothing, and instead offset is calculated from the host (Still Revit is insisting on calling this offset: Height offset from level).
I haven't added pipes to my tool so far as no one asked about it. I have to come back to this tool to add them - my code there needs some refactoring 😉
Happy to answer any questions 😉
If it doesn’t bother you, you can post an example of the code for moving through levels for OST_Walls, OST_StructuralColumns, OST_Stairs, OST_Ramps.
Sorry, but that would be cheating 😉
Two reasons:
1. The code works well, but I did it in my earlier days of programming. It needs some refactoring to make it more readable.
2. I am strongly against constant "reinvention" of the wheel, which is super prevalent in AEC. It stops us from growing and makes big players reluctant to invest money in it. My tool exists and cost $12 (which is nothing), so there shouldn't be any reason to develop something similar 😉
One hint: Ramps work almost like stairs, but at the same time they are really messed up. I posted about it on this forum.