Tape measure with numbers

Revit Bulk Unit Conversion Addin

Digital By Feb 22, 2024 No Comments

Journal scripting once offered a quick fix for converting Revit model and family units; it helped speed up the process, but it had the habit of failing on certain family types or dialogue boxes. To tackle this, I developed an add-in, which I shared on revit.com.au; which both overcame the errors that caused the journal to stumble, and was also significantly faster.

Unexpectedly, this tool found a wider audience than anticipated. Yet, as the Revit API evolved, the add-in did not, and so it no longer worked. After a recent request, I have open-sourced the old, non-working version of the code on GitHub. However, I’m not the kind of person who can leave these things sit there, which means it’s time for an upgrade.

Back in The Day

My previous approach to writing this code was to take a known-good metric and imperial file and export the unit settings in a way that wrote the code for me.

For example, this is how I set the length:

//UTLength
var foUTLength = units.GetFormatOptions(UnitType.UT_Length);
foUTLength.Accuracy = 1;
foUTLength.DisplayUnits = DisplayUnitType.DUT_MILLIMETERS;
units.SetFormatOptions(UnitType.UT_Length, foUTLength);Code language: C# (cs)

The macro I used extracted the details of the measurement type (Length) and the display unit type (DUT_MILLIMETERS) and extracted the accuracy value; the rest of the code was consistent across all measurement types, and I was able to spin up a functional add-in quickly.

Current Day

The API changed back in 2021/2022 and had a transitional period of 1 year in the API to be able to convert between the new and old methods easily. I won’t go into detail on the changes as they have been discussed extensively, but for those interested, both Jeremy and Konrad have posted great details on the changes and how to handle them.

Essentially, my code for adjusting length would need to change from what you see above, to this:

//UTLength
var foUTLength = units.GetFormatOptions(SpecTypeId.Length);
foUTLength.Accuracy = 1;
foUTLength.SetUnitTypeId(UnitTypeId.Millimeters);
units.SetFormatOptions(SpecTypeId.Length, foUTLength);Code language: C# (cs)

This might seem easy enough, but as there was only a 1-year grace period in the API, extracting the data from Revit to figure out what UnitType is equivalent to which SpecTypeId wasn’t going to be so easy. My addin changes over one hundred unit dispay types, I didn’t want to have to look them up and type them out manually. I was determined not to install Revit 2021 or ask for a loan of a machine that had it, so I had to give things a little more thought.

I started with some code from the Building Coder Samples, I created a few C# macros to help me extract a list of all ForgeTypeIds, and another to list out all valid ForgeTypeIds for each SpectTypeId.

Code (click to expand)
		public void ListForgeTypeIdsToFile()
		{
			string filePath = @"C:\temp\unitconfig.txt"; // Specify your file path here

			using (StreamWriter writer = new StreamWriter(filePath))
			{
				var spityp = typeof(SpecTypeId);
				var ps = spityp.GetProperties(BindingFlags.Public | BindingFlags.Static);

				Array.Sort(ps, (p1, p2) => p1.Name.CompareTo(p2.Name));

				writer.WriteLine(String.Format("{0} properties:", ps.Length));

				foreach (var pi in ps)
				{
					if (pi.PropertyType == typeof(ForgeTypeId))
					{
						var obj = pi.GetValue(null, null) as ForgeTypeId;
						writer.WriteLine(String.Format("{0}: {1}", pi.Name, obj.TypeId));
					}
				}

				var specs = UnitUtils.GetAllMeasurableSpecs();
				writer.WriteLine(String.Format("{0} specs:", specs.Count));

				Autodesk.Revit.DB.Units units_metric = new Autodesk.Revit.DB.Units(UnitSystem.Metric);

				foreach (var fti in specs)
				{
					writer.WriteLine(String.Format("{0}: {1}, {2}, {3}, {4}",
					                               fti, fti.TypeId,
					                               UnitUtils.GetTypeCatalogStringForSpec(fti),
					                               LabelUtils.GetLabelForSpec(fti),
					                               UnitFormatUtils.Format(units_metric, fti, 1, false)));
				}

				var units = UnitUtils.GetAllUnits();
				writer.WriteLine(String.Format("{0} units:", units.Count));

				foreach (var fti in units)
				{
					writer.WriteLine(String.Format("{0}: {1}, {2}, {3}",
					                               fti, fti.TypeId,
					                               UnitUtils.GetTypeCatalogStringForUnit(fti),
					                               LabelUtils.GetLabelForUnit(fti)));
				}
			}
		}Code language: C# (cs)

and

		public void ExportValidUnitsForSpecTypes()
		{
			//writer.WriteLine(String.Format("SpecTypeId: {0}, Valid UnitTypeId: {1}",specType,validUnit));
			
			Document doc = this.ActiveUIDocument.Document;
			var units = doc.GetUnits();
			var specTypes = UnitUtils.GetAllMeasurableSpecs();
			
			string filePath = @"C:\temp\validspecs.txt"; // Specify your file path here

			using (StreamWriter writer = new StreamWriter(filePath))
			{
				foreach (ForgeTypeId specType in specTypes)
				{
					IList<ForgeTypeId> validUnits = UnitUtils.GetValidUnits(specType); // Get valid units for each spec type
					foreach (ForgeTypeId validUnit in validUnits)
					{
						// Access the TypeId property to get the string identifier
						writer.WriteLine(String.Format("SpecTypeId: {0}, Valid UnitTypeId: {1}",specType.TypeId,validUnit.TypeId));
					}
				}
			}
		}Code language: C# (cs)

Which resulted in the following files:

In the above examples, I have the configuration properties for a valid Imperial file, and another with a list of all valid specs for each SpecTypeId, which is super handy if you want to change things.

From here we need to align all the UnitTypeIds that were listed with the UnitTypeId members from the API however the two lists aren’t a 1:1 equivelance, so a little finessing of the lists is required.

Figuring this out, though is easy enough with Excel by extracting the portion of the data from Revit

I could then compare the two strings quickly using a formula that checked the lowercase version of the value in column A against the lowercase version of the value in column B.

=IF(LOWER(A1)=LOWER(B1),1,0)

With the results of that formula and some basic conditional formatting, I could very quickly identify the discrepancies or misalignment between the two lists and validate that the data aligned and matched.

From here, we can take the data exported from the file we want to base our conversion configuration from, combine it with the data produced to look up the correct formatting of the UnitTypeId member, and generate working code using basic Excel functions.

In my Excel, I have created a named region “lookuptable” and used that in a VLOOKUP to build my code:

="//"&A1&"
var fo"&A1&" = units.GetFormatOptions(SpecTypeId."&A1&");
fo"&A1&"."&B1&";
fo"&A1&".SetUnitTypeId(UnitTypeId."&VLOOKUP(LEFT(D1,LEN(D1)-6),lookuptable,2,FALSE)&");
units.SetFormatOptions(SpecTypeId."&A1&", fo"&A1&");"Code language: PHP (php)

What we are doing here is

  • Value in column A becomes the suffix of the variable, and the SpecTypeId
  • The value in column B is the accuracy value
  • The lookup result from column D and the lookup table is the UnitTypeId

The result of the formula in column E is a working code snip that can be added to a macro or add-in.

Whether you’re looking to adapt a customised version to your specific needs, for either newer versions of Revit or for maintaining legacy projects, the updated add-ins are designed to streamline your workflows. Download the version that fits your needs:

Revit Unit Conversion Addin for Revit 2017-2021 (source code only)

Revit Unit Conversion Addin for Revit 2022-2024

Revit Unit Conversion Addin for Revit 2022-2024 Installer

No Comments

Leave a comment

Your email address will not be published. Required fields are marked *