diff --git a/src/cs/vim/Vim.Format.Tests/ElementParameterInfoServiceTests.cs b/src/cs/vim/Vim.Format.Tests/ElementParameterInfoServiceTests.cs
index 375de2cb..92f6e770 100644
--- a/src/cs/vim/Vim.Format.Tests/ElementParameterInfoServiceTests.cs
+++ b/src/cs/vim/Vim.Format.Tests/ElementParameterInfoServiceTests.cs
@@ -1,4 +1,5 @@
using NUnit.Framework;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -32,6 +33,7 @@ public static void TestElementParameterInfoService(string vimFilePath)
var levelInfos = infos.LevelInfos;
var elementLevelInfos = infos.ElementLevelInfos;
var elementMeasureInfos = infos.ElementMeasureInfos;
+ var elementIfcInfos = infos.ElementIfcInfos;
var parameterMeasureTypes = infos.ParameterMeasureTypes;
var validationTableSet = new EntityTableSet(
@@ -44,6 +46,7 @@ public static void TestElementParameterInfoService(string vimFilePath)
var elementInstanceCount = validationTableSet.ElementTable.RowCount;
Assert.AreEqual(elementInstanceCount, elementLevelInfos.Length);
Assert.AreEqual(elementInstanceCount, elementMeasureInfos.Length);
+ Assert.AreEqual(elementInstanceCount, elementIfcInfos.Length);
var parameterCount = validationTableSet.ParameterTable.RowCount;
Assert.AreEqual(parameterCount, parameterMeasureTypes.Length);
@@ -57,4 +60,22 @@ public static void TestElementParameterInfoService(string vimFilePath)
if (familyInstanceElementMap.Count > 0)
Assert.Greater(knownFamilyInstanceCount, 0);
}
+
+ [Test]
+ public static void TestIfcGuidParseRoundTrip()
+ {
+ var testGuids = Enumerable.Range(0, 10000).Select(i => Guid.NewGuid()).Prepend(Guid.Empty);
+
+ foreach (var guid in testGuids)
+ {
+ Assert.AreEqual(ElementIfcInfo.IfcGuidCanonicalLength, guid.ToString().Length);
+
+ var ifcGuid = ElementIfcInfo.ToIfcGuid(guid);
+ Assert.IsFalse(string.IsNullOrEmpty(ifcGuid), $"Converted IFC guid is null or empty. Source: {guid.ToString()}");
+ Assert.AreEqual(ElementIfcInfo.IfcGuidLength, ifcGuid.Length, $"Converted IFC must be 22 characters long. Source: {guid.ToString()} | IFC Guid: {ifcGuid}");
+ Assert.IsTrue(ifcGuid.All(c => ElementIfcInfo.Base64Chars.IndexOf(c) != -1), $"All IFC guid characters must be in the Base64Chars string. Source: {guid.ToString()} | IFC Guid: {ifcGuid}");
+ Assert.IsTrue(ElementIfcInfo.TryParseIfcGuidAsCanonicalGuid(ifcGuid, out var parsedGuid), $"Failed to parse IFC Guid. Source: {guid.ToString()} | IFC Guid: {ifcGuid}");
+ Assert.AreEqual(guid, parsedGuid);
+ }
+ }
}
diff --git a/src/cs/vim/Vim.Format/ElementParameterInfo/ElementIfcInfo.cs b/src/cs/vim/Vim.Format/ElementParameterInfo/ElementIfcInfo.cs
new file mode 100644
index 00000000..3ef47a38
--- /dev/null
+++ b/src/cs/vim/Vim.Format/ElementParameterInfo/ElementIfcInfo.cs
@@ -0,0 +1,188 @@
+using System;
+using System.Collections.Generic;
+using Vim.Format.ObjectModel;
+
+// SOME BACKGROUND INFORMATION ABOUT ELEMENT IFC GUIDS
+//
+// by: Martin Ashton, August 25, 2025
+//
+// - VIM Elements sourced from Revit may have the parameter IfcGUID, which defines the IFC GUID of the element (which is distinct from the value stored in Element.UniqueId)
+// - VIM Elements sourced from IFC files use the Element.UniqueId field to store the equivalent IfcGUID.
+// - This IfcGUID is a "compressed" 22 character case-sensitive string.
+// - This unfortunately does not play nicely with systems which merge records in a case-insensitive manner (ex: PowerBI)
+// - To resolve the casing issue, we expand the 22 character IfcGuid (if present) into its canonical GUID representation composed of 36 hexadecimal characters and dashes ('-').
+
+namespace Vim.Format.ElementParameterInfo
+{
+ ///
+ /// Convenience class which extracts IfcGuid from the Element's parameters or from its UniqueId.
+ ///
+ public class ElementIfcInfo : IElementIndex
+ {
+ public Element Element { get; }
+
+ public int GetElementIndexOrNone()
+ => Element.IndexOrDefault();
+
+ ///
+ /// A 22 character IFC encoded GUID composed of case-sensitive characters.
+ ///
+ public string IfcGuid { get; }
+
+ ///
+ /// The built-in Revit parameters which contain the IFC GUID.
+ ///
+ public readonly HashSet BuiltInIfcGuidParameterIds = new HashSet()
+ {
+ "-1019000", //IFC_GUID, "IfcGUID"
+ "-1019001", //IFC_TYPE_GUID, "Type IfcGUID"
+ "-1019002", //IFC_PROJECT_GUID, "IfcProject GUID"
+ "-1019003", //IFC_BUILDING_GUID, "IfcBuilding GUID"
+ "-1019004", //IFC_SITE_GUID, "IfcSite GUID"
+ };
+
+ ///
+ /// The expanded canonical Guid based on the 22 character IfcGuid.
+ ///
+ public Guid? IfcGuidCanonical { get; }
+
+ ///
+ /// Constructor.
+ ///
+ public ElementIfcInfo(
+ Element element,
+ ParameterTable parameterTable,
+ ElementIndexMaps elementIndexMaps)
+ {
+ Element = element;
+
+ var elementIndex = GetElementIndexOrNone();
+
+ var elementParameterIndices = elementIndexMaps.GetParameterIndicesFromElementIndex(elementIndex);
+
+ // 0. Initialize the properties to their default null values.
+ IfcGuid = null;
+ IfcGuidCanonical = null;
+
+ // 1. Check if the unique ID can be parsed from the element.UniqueId field.
+ var candidateIfcGuidFromUniqueId = element.UniqueId;
+ if (TryParseIfcGuidAsCanonicalGuid(candidateIfcGuidFromUniqueId, out var guidFromUniqueId))
+ {
+ IfcGuid = candidateIfcGuidFromUniqueId;
+ IfcGuidCanonical = guidFromUniqueId;
+ return;
+ }
+
+ // 2. Look for the relevant parameters associated to this element.
+ foreach (var parameterIndex in elementParameterIndices)
+ {
+ var p = parameterTable.Get(parameterIndex);
+ var d = p.ParameterDescriptor;
+ var builtInId = d.Guid; // This is the built-in ID of the parameter (not to be confused with the actual IFC GUID value we're looking for)
+
+ if (!BuiltInIfcGuidParameterIds.Contains(builtInId) &&
+ !d.Name.Equals("IfcGUID", StringComparison.InvariantCultureIgnoreCase))
+ {
+ // This is not the parameter you are looking for.
+ continue;
+ }
+
+ var (candidateIfcGuidFromParameter, _) = p.Values; // check the native value.
+
+ if (TryParseIfcGuidAsCanonicalGuid(candidateIfcGuidFromParameter, out var ifcGuidCanonical))
+ {
+ IfcGuid = candidateIfcGuidFromParameter;
+ IfcGuidCanonical = ifcGuidCanonical;
+ }
+
+ // We have just visited a IfcGUID parameter, so we can end our search.
+ break;
+ }
+ }
+
+ // Characters used in the 22-char encoding
+ public const string Base64Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$";
+ public const uint IfcGuidLength = 22;
+ public const uint IfcGuidCanonicalLength = 36;
+
+ ///
+ /// Converts a 22-character IFC GUID into a System.Guid
+ ///
+ public static bool TryParseIfcGuidAsCanonicalGuid(string ifcGuid, out Guid guid)
+ {
+ guid = Guid.Empty;
+ if (string.IsNullOrEmpty(ifcGuid) || ifcGuid.Length != IfcGuidLength)
+ return false;
+
+ var bytes = new byte[16];
+ var bitPos = 0;
+ var bytePos = 0;
+ var value = 0;
+ var bitsLeft = 0;
+
+ foreach (var c in ifcGuid)
+ {
+ var index = Base64Chars.IndexOf(c);
+ if (index < 0)
+ return false;
+
+ value = (value << 6) | index;
+ bitsLeft += 6;
+
+ if (bitsLeft >= 8)
+ {
+ bitsLeft -= 8;
+ bytes[bytePos++] = (byte)((value >> bitsLeft) & 0xFF);
+ if (bytePos == 16)
+ break;
+ }
+ }
+
+ guid = new Guid(bytes);
+ return true;
+ }
+
+ ///
+ /// Converts a Guid into the 22-character IFC GUID format
+ ///
+ public static string ToIfcGuid(Guid guid)
+ {
+ if (guid == Guid.Empty)
+ {
+ return "0000000000000000000000"; // 22 characters of 0s
+ }
+
+ var bytes = guid.ToByteArray();
+
+ var value = 0;
+ var bitsLeft = 0;
+ var result = new char[IfcGuidLength];
+ var charPos = 0;
+
+ foreach (var b in bytes)
+ {
+ value = (value << 8) | b;
+ bitsLeft += 8;
+
+ while (bitsLeft >= 6)
+ {
+ bitsLeft -= 6;
+ result[charPos++] = Base64Chars[(value >> bitsLeft) & 0x3F];
+ }
+ }
+
+ // handle remaining bits (pad if necessary)
+ if (charPos < IfcGuidLength)
+ {
+ if (bitsLeft > 0)
+ result[charPos++] = Base64Chars[(value << (6 - bitsLeft)) & 0x3F];
+
+ // pad with zeroes if still short (shouldn’t normally happen except for Guid.Empty)
+ while (charPos < IfcGuidLength)
+ result[charPos++] = Base64Chars[0];
+ }
+
+ return new string(result);
+ }
+ }
+}
diff --git a/src/cs/vim/Vim.Format/ElementParameterInfo/ElementLevelInfo.cs b/src/cs/vim/Vim.Format/ElementParameterInfo/ElementLevelInfo.cs
index c894503b..63133fd4 100644
--- a/src/cs/vim/Vim.Format/ElementParameterInfo/ElementLevelInfo.cs
+++ b/src/cs/vim/Vim.Format/ElementParameterInfo/ElementLevelInfo.cs
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Diagnostics;
using Vim.Format.ObjectModel;
-using Vim.Math3d;
using Vim.Util;
// ReSharper disable InconsistentNaming
@@ -64,6 +63,7 @@ public enum BuildingStoryGeometryContainment
CrossingAbove = 4, // lvlLow < min < lvlHi < max
CompletelyAbove = 5, // lvlLow < lvlHi < min < max
SpanningBelowAndAbove = 6, // min < lvlLow < lvlHi < max
+ NoGeometry = 7, // The element does not have geometry.
}
public class ElementLevelInfo : IElementIndex
@@ -378,9 +378,13 @@ private static BuildingStoryGeometryContainment GetBuildingStoryGeometryContainm
// Note: Level.ProjectElevation is relative to the internal scene origin (0,0,0), and so is the vim scene's geometry.
var elementGeometryInfo = elementGeometryMap.ElementAtOrDefault(elementIndex);
+
var hasGeometry = elementGeometryInfo?.HasGeometry ?? false;
- var bb = hasGeometry ? elementGeometryInfo.WorldSpaceBoundingBox : AABox.Empty;
- var bbIsValid = hasGeometry && bb.IsValid;
+ if (!hasGeometry)
+ return BuildingStoryGeometryContainment.NoGeometry;
+
+ var bb = elementGeometryInfo.WorldSpaceBoundingBox;
+ var bbIsValid = bb.IsValid;
var bbMin = bb.Min.Z;
var bbMax = bb.Max.Z;
diff --git a/src/cs/vim/Vim.Format/ElementParameterInfo/ElementParameterInfoService.cs b/src/cs/vim/Vim.Format/ElementParameterInfo/ElementParameterInfoService.cs
index cc01cd1e..b509d8e5 100644
--- a/src/cs/vim/Vim.Format/ElementParameterInfo/ElementParameterInfoService.cs
+++ b/src/cs/vim/Vim.Format/ElementParameterInfo/ElementParameterInfoService.cs
@@ -15,6 +15,7 @@ public struct ElementParameterInfo
public ElementLevelInfo[] ElementLevelInfos;
public ElementMeasureInfo[] ElementMeasureInfos;
public MeasureType[] ParameterMeasureTypes;
+ public ElementIfcInfo[] ElementIfcInfos;
}
///
@@ -86,12 +87,15 @@ n is TableNames.Level ||
var elementMeasureInfos = CreateElementMeasureInfos(elementTable, parameterTable, parameterMeasureTypes, elementIndexMaps);
+ var elementIfcInfos = CreateElementIfcInfos(elementTable, parameterTable, elementIndexMaps);
+
return new ElementParameterInfo
{
LevelInfos = levelInfos,
ElementLevelInfos = elementLevelInfos,
ElementMeasureInfos = elementMeasureInfos,
- ParameterMeasureTypes = parameterMeasureTypes
+ ParameterMeasureTypes = parameterMeasureTypes,
+ ElementIfcInfos = elementIfcInfos,
};
}
@@ -215,5 +219,14 @@ public static ElementMeasureInfo[] CreateElementMeasureInfos(
.AsParallel()
.Select(e => new ElementMeasureInfo(e, parameterTable, parameterMeasureTypes, elementIndexMaps))
.ToArray();
+
+ public static ElementIfcInfo[] CreateElementIfcInfos(
+ ElementTable elementTable,
+ ParameterTable parameterTable,
+ ElementIndexMaps elementIndexMaps)
+ => elementTable
+ .AsParallel()
+ .Select(e => new ElementIfcInfo(e, parameterTable, elementIndexMaps))
+ .ToArray();
}
}
diff --git a/src/cs/vim/Vim.Format/Vim.Format.csproj b/src/cs/vim/Vim.Format/Vim.Format.csproj
index 09427dd0..ef6beba7 100644
--- a/src/cs/vim/Vim.Format/Vim.Format.csproj
+++ b/src/cs/vim/Vim.Format/Vim.Format.csproj
@@ -10,7 +10,7 @@
GitHub
true
MIT
- 1.7.0
+ 1.8.0
true
true
true