Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/cs/vim/Vim.Format.Tests/ElementParameterInfoServiceTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -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(
Expand All @@ -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);
Expand All @@ -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);
}
}
}
188 changes: 188 additions & 0 deletions src/cs/vim/Vim.Format/ElementParameterInfo/ElementIfcInfo.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Convenience class which extracts IfcGuid from the Element's parameters or from its UniqueId.
/// </summary>
public class ElementIfcInfo : IElementIndex
{
public Element Element { get; }

public int GetElementIndexOrNone()
=> Element.IndexOrDefault();

/// <summary>
/// A 22 character IFC encoded GUID composed of case-sensitive characters.
/// </summary>
public string IfcGuid { get; }

/// <summary>
/// The built-in Revit parameters which contain the IFC GUID.
/// </summary>
public readonly HashSet<string> BuiltInIfcGuidParameterIds = new HashSet<string>()
{
"-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"
};

/// <summary>
/// The expanded canonical Guid based on the 22 character IfcGuid.
/// </summary>
public Guid? IfcGuidCanonical { get; }

/// <summary>
/// Constructor.
/// </summary>
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;

/// <summary>
/// Converts a 22-character IFC GUID into a System.Guid
/// </summary>
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;

Check warning on line 118 in src/cs/vim/Vim.Format/ElementParameterInfo/ElementIfcInfo.cs

View workflow job for this annotation

GitHub Actions / build_cs_merged / merge_and_run

The variable 'bitPos' is assigned but its value is never used

Check warning on line 118 in src/cs/vim/Vim.Format/ElementParameterInfo/ElementIfcInfo.cs

View workflow job for this annotation

GitHub Actions / test_cs_merged / merge_and_run

The variable 'bitPos' is assigned but its value is never used
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;
}

/// <summary>
/// Converts a Guid into the 22-character IFC GUID format
/// </summary>
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);
}
}
}
10 changes: 7 additions & 3 deletions src/cs/vim/Vim.Format/ElementParameterInfo/ElementLevelInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Diagnostics;
using Vim.Format.ObjectModel;
using Vim.Math3d;
using Vim.Util;

// ReSharper disable InconsistentNaming
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public struct ElementParameterInfo
public ElementLevelInfo[] ElementLevelInfos;
public ElementMeasureInfo[] ElementMeasureInfos;
public MeasureType[] ParameterMeasureTypes;
public ElementIfcInfo[] ElementIfcInfos;
}

/// <summary>
Expand Down Expand Up @@ -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,
};
}

Expand Down Expand Up @@ -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();
}
}
2 changes: 1 addition & 1 deletion src/cs/vim/Vim.Format/Vim.Format.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<RepositoryType>GitHub</RepositoryType>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Version>1.7.0</Version>
<Version>1.8.0</Version>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
Expand Down
Loading