Skip to content

Commit 5b89c26

Browse files
authored
Merge pull request #56 from vimaec/mavimaec/v1.2.141.14
Mavimaec/v1.2.141.14
2 parents 44a1a5f + 65b80d1 commit 5b89c26

5 files changed

Lines changed: 231 additions & 5 deletions

File tree

src/cs/vim/Vim.Format.Tests/ElementParameterInfoServiceTests.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using NUnit.Framework;
2+
using System;
23
using System.Collections.Generic;
34
using System.IO;
45
using System.Linq;
@@ -32,6 +33,7 @@ public static void TestElementParameterInfoService(string vimFilePath)
3233
var levelInfos = infos.LevelInfos;
3334
var elementLevelInfos = infos.ElementLevelInfos;
3435
var elementMeasureInfos = infos.ElementMeasureInfos;
36+
var elementIfcInfos = infos.ElementIfcInfos;
3537
var parameterMeasureTypes = infos.ParameterMeasureTypes;
3638

3739
var validationTableSet = new EntityTableSet(
@@ -44,6 +46,7 @@ public static void TestElementParameterInfoService(string vimFilePath)
4446
var elementInstanceCount = validationTableSet.ElementTable.RowCount;
4547
Assert.AreEqual(elementInstanceCount, elementLevelInfos.Length);
4648
Assert.AreEqual(elementInstanceCount, elementMeasureInfos.Length);
49+
Assert.AreEqual(elementInstanceCount, elementIfcInfos.Length);
4750

4851
var parameterCount = validationTableSet.ParameterTable.RowCount;
4952
Assert.AreEqual(parameterCount, parameterMeasureTypes.Length);
@@ -57,4 +60,22 @@ public static void TestElementParameterInfoService(string vimFilePath)
5760
if (familyInstanceElementMap.Count > 0)
5861
Assert.Greater(knownFamilyInstanceCount, 0);
5962
}
63+
64+
[Test]
65+
public static void TestIfcGuidParseRoundTrip()
66+
{
67+
var testGuids = Enumerable.Range(0, 10000).Select(i => Guid.NewGuid()).Prepend(Guid.Empty);
68+
69+
foreach (var guid in testGuids)
70+
{
71+
Assert.AreEqual(ElementIfcInfo.IfcGuidCanonicalLength, guid.ToString().Length);
72+
73+
var ifcGuid = ElementIfcInfo.ToIfcGuid(guid);
74+
Assert.IsFalse(string.IsNullOrEmpty(ifcGuid), $"Converted IFC guid is null or empty. Source: {guid.ToString()}");
75+
Assert.AreEqual(ElementIfcInfo.IfcGuidLength, ifcGuid.Length, $"Converted IFC must be 22 characters long. Source: {guid.ToString()} | IFC Guid: {ifcGuid}");
76+
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}");
77+
Assert.IsTrue(ElementIfcInfo.TryParseIfcGuidAsCanonicalGuid(ifcGuid, out var parsedGuid), $"Failed to parse IFC Guid. Source: {guid.ToString()} | IFC Guid: {ifcGuid}");
78+
Assert.AreEqual(guid, parsedGuid);
79+
}
80+
}
6081
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Vim.Format.ObjectModel;
4+
5+
// SOME BACKGROUND INFORMATION ABOUT ELEMENT IFC GUIDS
6+
//
7+
// by: Martin Ashton, August 25, 2025
8+
//
9+
// - 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)
10+
// - VIM Elements sourced from IFC files use the Element.UniqueId field to store the equivalent IfcGUID.
11+
// - This IfcGUID is a "compressed" 22 character case-sensitive string.
12+
// - This unfortunately does not play nicely with systems which merge records in a case-insensitive manner (ex: PowerBI)
13+
// - 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 ('-').
14+
15+
namespace Vim.Format.ElementParameterInfo
16+
{
17+
/// <summary>
18+
/// Convenience class which extracts IfcGuid from the Element's parameters or from its UniqueId.
19+
/// </summary>
20+
public class ElementIfcInfo : IElementIndex
21+
{
22+
public Element Element { get; }
23+
24+
public int GetElementIndexOrNone()
25+
=> Element.IndexOrDefault();
26+
27+
/// <summary>
28+
/// A 22 character IFC encoded GUID composed of case-sensitive characters.
29+
/// </summary>
30+
public string IfcGuid { get; }
31+
32+
/// <summary>
33+
/// The built-in Revit parameters which contain the IFC GUID.
34+
/// </summary>
35+
public readonly HashSet<string> BuiltInIfcGuidParameterIds = new HashSet<string>()
36+
{
37+
"-1019000", //IFC_GUID, "IfcGUID"
38+
"-1019001", //IFC_TYPE_GUID, "Type IfcGUID"
39+
"-1019002", //IFC_PROJECT_GUID, "IfcProject GUID"
40+
"-1019003", //IFC_BUILDING_GUID, "IfcBuilding GUID"
41+
"-1019004", //IFC_SITE_GUID, "IfcSite GUID"
42+
};
43+
44+
/// <summary>
45+
/// The expanded canonical Guid based on the 22 character IfcGuid.
46+
/// </summary>
47+
public Guid? IfcGuidCanonical { get; }
48+
49+
/// <summary>
50+
/// Constructor.
51+
/// </summary>
52+
public ElementIfcInfo(
53+
Element element,
54+
ParameterTable parameterTable,
55+
ElementIndexMaps elementIndexMaps)
56+
{
57+
Element = element;
58+
59+
var elementIndex = GetElementIndexOrNone();
60+
61+
var elementParameterIndices = elementIndexMaps.GetParameterIndicesFromElementIndex(elementIndex);
62+
63+
// 0. Initialize the properties to their default null values.
64+
IfcGuid = null;
65+
IfcGuidCanonical = null;
66+
67+
// 1. Check if the unique ID can be parsed from the element.UniqueId field.
68+
var candidateIfcGuidFromUniqueId = element.UniqueId;
69+
if (TryParseIfcGuidAsCanonicalGuid(candidateIfcGuidFromUniqueId, out var guidFromUniqueId))
70+
{
71+
IfcGuid = candidateIfcGuidFromUniqueId;
72+
IfcGuidCanonical = guidFromUniqueId;
73+
return;
74+
}
75+
76+
// 2. Look for the relevant parameters associated to this element.
77+
foreach (var parameterIndex in elementParameterIndices)
78+
{
79+
var p = parameterTable.Get(parameterIndex);
80+
var d = p.ParameterDescriptor;
81+
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)
82+
83+
if (!BuiltInIfcGuidParameterIds.Contains(builtInId) &&
84+
!d.Name.Equals("IfcGUID", StringComparison.InvariantCultureIgnoreCase))
85+
{
86+
// This is not the parameter you are looking for.
87+
continue;
88+
}
89+
90+
var (candidateIfcGuidFromParameter, _) = p.Values; // check the native value.
91+
92+
if (TryParseIfcGuidAsCanonicalGuid(candidateIfcGuidFromParameter, out var ifcGuidCanonical))
93+
{
94+
IfcGuid = candidateIfcGuidFromParameter;
95+
IfcGuidCanonical = ifcGuidCanonical;
96+
}
97+
98+
// We have just visited a IfcGUID parameter, so we can end our search.
99+
break;
100+
}
101+
}
102+
103+
// Characters used in the 22-char encoding
104+
public const string Base64Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$";
105+
public const uint IfcGuidLength = 22;
106+
public const uint IfcGuidCanonicalLength = 36;
107+
108+
/// <summary>
109+
/// Converts a 22-character IFC GUID into a System.Guid
110+
/// </summary>
111+
public static bool TryParseIfcGuidAsCanonicalGuid(string ifcGuid, out Guid guid)
112+
{
113+
guid = Guid.Empty;
114+
if (string.IsNullOrEmpty(ifcGuid) || ifcGuid.Length != IfcGuidLength)
115+
return false;
116+
117+
var bytes = new byte[16];
118+
var bitPos = 0;
119+
var bytePos = 0;
120+
var value = 0;
121+
var bitsLeft = 0;
122+
123+
foreach (var c in ifcGuid)
124+
{
125+
var index = Base64Chars.IndexOf(c);
126+
if (index < 0)
127+
return false;
128+
129+
value = (value << 6) | index;
130+
bitsLeft += 6;
131+
132+
if (bitsLeft >= 8)
133+
{
134+
bitsLeft -= 8;
135+
bytes[bytePos++] = (byte)((value >> bitsLeft) & 0xFF);
136+
if (bytePos == 16)
137+
break;
138+
}
139+
}
140+
141+
guid = new Guid(bytes);
142+
return true;
143+
}
144+
145+
/// <summary>
146+
/// Converts a Guid into the 22-character IFC GUID format
147+
/// </summary>
148+
public static string ToIfcGuid(Guid guid)
149+
{
150+
if (guid == Guid.Empty)
151+
{
152+
return "0000000000000000000000"; // 22 characters of 0s
153+
}
154+
155+
var bytes = guid.ToByteArray();
156+
157+
var value = 0;
158+
var bitsLeft = 0;
159+
var result = new char[IfcGuidLength];
160+
var charPos = 0;
161+
162+
foreach (var b in bytes)
163+
{
164+
value = (value << 8) | b;
165+
bitsLeft += 8;
166+
167+
while (bitsLeft >= 6)
168+
{
169+
bitsLeft -= 6;
170+
result[charPos++] = Base64Chars[(value >> bitsLeft) & 0x3F];
171+
}
172+
}
173+
174+
// handle remaining bits (pad if necessary)
175+
if (charPos < IfcGuidLength)
176+
{
177+
if (bitsLeft > 0)
178+
result[charPos++] = Base64Chars[(value << (6 - bitsLeft)) & 0x3F];
179+
180+
// pad with zeroes if still short (shouldn’t normally happen except for Guid.Empty)
181+
while (charPos < IfcGuidLength)
182+
result[charPos++] = Base64Chars[0];
183+
}
184+
185+
return new string(result);
186+
}
187+
}
188+
}

src/cs/vim/Vim.Format/ElementParameterInfo/ElementLevelInfo.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using System.Collections.Generic;
33
using System.Diagnostics;
44
using Vim.Format.ObjectModel;
5-
using Vim.Math3d;
65
using Vim.Util;
76

87
// ReSharper disable InconsistentNaming
@@ -64,6 +63,7 @@ public enum BuildingStoryGeometryContainment
6463
CrossingAbove = 4, // lvlLow < min < lvlHi < max
6564
CompletelyAbove = 5, // lvlLow < lvlHi < min < max
6665
SpanningBelowAndAbove = 6, // min < lvlLow < lvlHi < max
66+
NoGeometry = 7, // The element does not have geometry.
6767
}
6868

6969
public class ElementLevelInfo : IElementIndex
@@ -378,9 +378,13 @@ private static BuildingStoryGeometryContainment GetBuildingStoryGeometryContainm
378378

379379
// Note: Level.ProjectElevation is relative to the internal scene origin (0,0,0), and so is the vim scene's geometry.
380380
var elementGeometryInfo = elementGeometryMap.ElementAtOrDefault(elementIndex);
381+
381382
var hasGeometry = elementGeometryInfo?.HasGeometry ?? false;
382-
var bb = hasGeometry ? elementGeometryInfo.WorldSpaceBoundingBox : AABox.Empty;
383-
var bbIsValid = hasGeometry && bb.IsValid;
383+
if (!hasGeometry)
384+
return BuildingStoryGeometryContainment.NoGeometry;
385+
386+
var bb = elementGeometryInfo.WorldSpaceBoundingBox;
387+
var bbIsValid = bb.IsValid;
384388
var bbMin = bb.Min.Z;
385389
var bbMax = bb.Max.Z;
386390

src/cs/vim/Vim.Format/ElementParameterInfo/ElementParameterInfoService.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public struct ElementParameterInfo
1515
public ElementLevelInfo[] ElementLevelInfos;
1616
public ElementMeasureInfo[] ElementMeasureInfos;
1717
public MeasureType[] ParameterMeasureTypes;
18+
public ElementIfcInfo[] ElementIfcInfos;
1819
}
1920

2021
/// <summary>
@@ -86,12 +87,15 @@ n is TableNames.Level ||
8687

8788
var elementMeasureInfos = CreateElementMeasureInfos(elementTable, parameterTable, parameterMeasureTypes, elementIndexMaps);
8889

90+
var elementIfcInfos = CreateElementIfcInfos(elementTable, parameterTable, elementIndexMaps);
91+
8992
return new ElementParameterInfo
9093
{
9194
LevelInfos = levelInfos,
9295
ElementLevelInfos = elementLevelInfos,
9396
ElementMeasureInfos = elementMeasureInfos,
94-
ParameterMeasureTypes = parameterMeasureTypes
97+
ParameterMeasureTypes = parameterMeasureTypes,
98+
ElementIfcInfos = elementIfcInfos,
9599
};
96100
}
97101

@@ -215,5 +219,14 @@ public static ElementMeasureInfo[] CreateElementMeasureInfos(
215219
.AsParallel()
216220
.Select(e => new ElementMeasureInfo(e, parameterTable, parameterMeasureTypes, elementIndexMaps))
217221
.ToArray();
222+
223+
public static ElementIfcInfo[] CreateElementIfcInfos(
224+
ElementTable elementTable,
225+
ParameterTable parameterTable,
226+
ElementIndexMaps elementIndexMaps)
227+
=> elementTable
228+
.AsParallel()
229+
.Select(e => new ElementIfcInfo(e, parameterTable, elementIndexMaps))
230+
.ToArray();
218231
}
219232
}

src/cs/vim/Vim.Format/Vim.Format.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<RepositoryType>GitHub</RepositoryType>
1111
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
1212
<PackageLicenseExpression>MIT</PackageLicenseExpression>
13-
<Version>1.7.0</Version>
13+
<Version>1.8.0</Version>
1414
<PublishRepositoryUrl>true</PublishRepositoryUrl>
1515
<EmbedUntrackedSources>true</EmbedUntrackedSources>
1616
<IncludeSymbols>true</IncludeSymbols>

0 commit comments

Comments
 (0)