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