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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ API between minor releases.

## [Unreleased]

### Breaking changes

- Legacy IFC profile: the `GlobalId` of a type-level `IfcRelAssociatesMaterial` relation is now seeded from the material element id list (`type-materials:{materialIds}`, as `materials:{materialIds}` already is for product-level relations, and `type-layer-set:{seed}` from the host type id or layer-set group key) instead of the STEP entity number of the relating material, material list or layer set. Type-level material relation `GlobalId`s in the legacy profile change once and no longer shift when entities are added or removed earlier in the file; product-level material relations and the current IFC profile are unchanged. (Closes #88)

### Added

- Default output, current IFC profile: every beam with a straight drive curve gets `Pset_BeamCommon.Slope`, the angle in degrees of its drive curve to the horizontal (0 for a horizontal beam, never negative), whether or not BaseQuantities are exported and whatever its body; its quantities are unchanged. A beam without a straight drive curve gets no Slope.
Expand Down
47 changes: 25 additions & 22 deletions src/RvtIO/Ifc/IfcExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5834,7 +5834,7 @@ int EnsureUnnamedMaterial() =>
}

var layerSetByKey = new Dictionary<string, (int LayerSetId, double TotalThicknessMm)>();
var layerSetByProductEntity = new Dictionary<int, int>();
var layerSetByProductEntity = new Dictionary<int, (int EntityId, string Seed)>();
var layerUsageProductEntities = new HashSet<int>();
var layeredProducts = exportedIfcElements
.Select(pair => (ElementId: pair.Key, Product: pair.Value,
Expand Down Expand Up @@ -5913,7 +5913,9 @@ int EnsureUnnamedMaterial() =>
layerSetByKey[layerSetKey] = layerSet;
}

foreach (var item in group) layerSetByProductEntity[item.Product.entityId] = layerSet.LayerSetId;
string layerSetSeed = first.Native is { } nativeSeed
? nativeSeed.TypeId.ToString(CultureInfo.InvariantCulture) : group.Key;
foreach (var item in group) layerSetByProductEntity[item.Product.entityId] = (layerSet.LayerSetId, layerSetSeed);
var associated = CurrentProfile ? group.Where(item => item.Product.ifcType != "IFCWALL").ToList() : group.ToList();
if (associated.Count == 0) continue;

Expand Down Expand Up @@ -6152,7 +6154,7 @@ int EnsureUnnamedMaterial() =>
for (int part = 0; part < (partMaterials?.Count ?? 1); part++)
{
var partIds = partMaterials == null ? materialElementIds : [partMaterials[part]];
string materialKey = string.Join(',', partIds);
string materialKey = string.Join(',', partIds.Select(id => id.ToString(CultureInfo.InvariantCulture)));
if (!directMaterialProducts.TryGetValue(materialKey, out var association))
association = (partIds, new List<int>());
if (pieceBased && product.ifcType == "IFCSLAB" && element.CategoryId == -2001300)
Expand All @@ -6166,7 +6168,7 @@ int EnsureUnnamedMaterial() =>
}
}

var relatingMaterialByProductEntity = new Dictionary<int, int>();
var relatingMaterialByProductEntity = new Dictionary<int, (int EntityId, string Seed)>();
var directRelationByRelating = new Dictionary<int, (int Index, string GuidArg, List<int> Products)>();
foreach (var (materialKey, association) in directMaterialProducts)
{
Expand All @@ -6177,9 +6179,9 @@ int EnsureUnnamedMaterial() =>
? materialIds[0]
: w.Add("IFCMATERIALLIST", StepWriter.List(materialIds.Select(R)));
foreach (int productEntityId in association.ProductIds)
relatingMaterialByProductEntity[productEntityId] = relatingMaterialId;
relatingMaterialByProductEntity[productEntityId] = (relatingMaterialId, materialKey);
foreach (int productEntityId in typeOnlyMaterialProducts.GetValueOrDefault(materialKey) ?? [])
relatingMaterialByProductEntity[productEntityId] = relatingMaterialId;
relatingMaterialByProductEntity[productEntityId] = (relatingMaterialId, materialKey);
if (association.ProductIds.Count > 0)
{
string guidArg = S(IfcGuid.FromName($"materials:{materialKey}"));
Expand Down Expand Up @@ -67077,7 +67079,7 @@ internal static (string? Full, string? Own) TypeObjectNameParts(Element typeElem
/// </summary>
private static void AssociateTypeObjectMaterials(
StepWriter w, int ownerHistId, int startIndex,
IReadOnlyDictionary<int, int> relatingMaterialByProductEntity, HostLayerSets? layerSets = null,
IReadOnlyDictionary<int, (int EntityId, string Seed)> relatingMaterialByProductEntity, HostLayerSets? layerSets = null,
IReadOnlyDictionary<int, (int Index, string GuidArg, List<int> Products)>? directRelations = null)
{
var typeClassByEntity = new Dictionary<int, string>();
Expand All @@ -67096,8 +67098,8 @@ private static void AssociateTypeObjectMaterials(
firstMemberByType.Add((typeEntityId, firstMember));
}

var typesByRelatingMaterial = new SortedDictionary<int, List<int>>();
var typesByLayerSet = new SortedDictionary<int, List<int>>();
var typesByRelatingMaterial = new SortedDictionary<int, (string Seed, List<int> Types)>();
var typesByLayerSet = new SortedDictionary<int, (string Seed, List<int> Types)>();
foreach (var (typeEntityId, firstMember) in firstMemberByType)
{
if (layerSets != null && typeClassByEntity.GetValueOrDefault(typeEntityId) == "IFCSTAIRFLIGHTTYPE") continue;
Expand All @@ -67106,10 +67108,11 @@ private static void AssociateTypeObjectMaterials(
// usage (the host-object export with the type handle); ceilings and roofs do not.
if (layerSets != null && typeClassByEntity.GetValueOrDefault(typeEntityId) is { } hostTypeClass
&& (hostTypeClass == "IFCWALLTYPE" || hostTypeClass == "IFCSLABTYPE" && layerSets.UsageProducts.Contains(firstMember))
&& layerSets.ByProduct.TryGetValue(firstMember, out int typeLayerSet))
&& layerSets.ByProduct.TryGetValue(firstMember, out var typeLayerSet))
{
if (!typesByLayerSet.TryGetValue(typeLayerSet, out var typed)) typesByLayerSet[typeLayerSet] = typed = new List<int>();
typed.Add(typeEntityId);
if (!typesByLayerSet.TryGetValue(typeLayerSet.EntityId, out var typed))
typesByLayerSet[typeLayerSet.EntityId] = typed = (typeLayerSet.Seed, new List<int>());
typed.Types.Add(typeEntityId);
continue;
}
// Host objects (walls, floors, ceilings, roofs) give their type the layer set instead.
Expand All @@ -67119,13 +67122,13 @@ private static void AssociateTypeObjectMaterials(
bool hostType = hostOrFamilyTypeClass is null or "IFCCURTAINWALLTYPE" or "IFCWALLTYPE"
or "IFCSLABTYPE" or "IFCCOVERINGTYPE" or "IFCROOFTYPE";
if (hostType && !slabFamilyType
|| !relatingMaterialByProductEntity.TryGetValue(firstMember, out int relatingMaterial))
|| !relatingMaterialByProductEntity.TryGetValue(firstMember, out var relatingMaterial))
continue;
if (!typesByRelatingMaterial.TryGetValue(relatingMaterial, out var types))
typesByRelatingMaterial[relatingMaterial] = types = new List<int>();
types.Add(typeEntityId);
if (!typesByRelatingMaterial.TryGetValue(relatingMaterial.EntityId, out var types))
typesByRelatingMaterial[relatingMaterial.EntityId] = types = (relatingMaterial.Seed, new List<int>());
types.Types.Add(typeEntityId);
}
foreach (var (relatingMaterial, typeEntityIds) in typesByRelatingMaterial)
foreach (var (relatingMaterial, (materialSeed, typeEntityIds)) in typesByRelatingMaterial)
{
if (directRelations != null && directRelations.TryGetValue(relatingMaterial, out var direct))
{
Expand All @@ -67134,12 +67137,12 @@ private static void AssociateTypeObjectMaterials(
continue;
}
w.Add("IFCRELASSOCIATESMATERIAL",
$"{S(IfcGuid.FromName($"type-materials:{relatingMaterial}"))},{R(ownerHistId)},$,$,"
$"{S(IfcGuid.FromName($"type-materials:{materialSeed}"))},{R(ownerHistId)},$,$,"
+ $"{StepWriter.List(typeEntityIds.Select(R))},{R(relatingMaterial)}");
}
foreach (var (layerSet, typeEntityIds) in typesByLayerSet)
foreach (var (layerSet, (layerSetSeed, typeEntityIds)) in typesByLayerSet)
w.Add("IFCRELASSOCIATESMATERIAL",
$"{S(IfcGuid.FromName($"type-layer-set:{layerSet}"))},{R(ownerHistId)},$,$,"
$"{S(IfcGuid.FromName($"type-layer-set:{layerSetSeed}"))},{R(ownerHistId)},$,$,"
+ $"{StepWriter.List(typeEntityIds.Select(R))},{R(layerSet)}");
}

Expand Down Expand Up @@ -67186,8 +67189,8 @@ private static int WriteInsulationCoverings(
/// The layer sets of the host objects of one export: by product entity, and which products got
/// a usage.
/// </summary>
private sealed record HostLayerSets(IReadOnlyDictionary<int, int> ByProduct, IReadOnlySet<int> UsageProducts,
IReadOnlySet<int> TypeOnlyProducts);
private sealed record HostLayerSets(IReadOnlyDictionary<int, (int EntityId, string Seed)> ByProduct,
IReadOnlySet<int> UsageProducts, IReadOnlySet<int> TypeOnlyProducts);

/// <summary>
/// The connector export creates a distribution port for every unconnected connector of an MEP
Expand Down
122 changes: 120 additions & 2 deletions tests/RvtIO.Tests/IfcExporterTypeObjectsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -523,8 +523,126 @@ public void ComponentType_ReceivesTheMaterialOfItsFirstInstance()
.Select(id => EntityArgs(ifc, id, "IFCRELASSOCIATESMATERIAL")).ToList();
Assert.Equal(2, relations.Count);
Assert.All(relations, args => Assert.Equal($"#{material}", args[5]));
Assert.Contains(relations, args => args[4] == $"(#{memberEntity})");
Assert.Contains(relations, args => args[4] == $"(#{typeEntity})");
var productRel = Assert.Single(relations, args => args[4] == $"(#{memberEntity})");
var typeRel = Assert.Single(relations, args => args[4] == $"(#{typeEntity})");
Assert.Equal($"'{IfcGuid.FromName("materials:0")}'", productRel[0]);
Assert.Equal($"'{IfcGuid.FromName("type-materials:0")}'", typeRel[0]);
}

[Fact]
public void LegacyTypeMaterialRelationGlobalIds_StayStableWhenEarlierStepEntitiesShift()
{
Element MaterialElement(int id, string name) => new()
{
Id = id,
TypeId = -1,
IsBlockOnly = true,
CategoryId = -2000700,
Category = "OST_Materials",
Name = name,
HasShadingAppearance = true,
Parameters =
[
new Parameter { Id = id + 1, Definition = new Definition { Name = "MATERIAL_PARAM_COLOR" }, Value = 0x112233, StorageType = StorageType.Integer },
new Parameter { Id = id + 2, Definition = new Definition { Name = "MATERIAL_PARAM_SHININESS" }, Value = 25, StorageType = StorageType.Integer },
],
};

Element[] BuildBaseElements()
{
var steel = MaterialElement(700, "Steel");
var glass = MaterialElement(710, "Glass");
var wood = MaterialElement(720, "Wood");

var singleType = TypeElement(500, "SingleType", "MullionFamily");
singleType.CategoryId = -2000171;
singleType.Parameters.Add(new Parameter { Id = -1002107, StorageType = StorageType.ElementId, Value = 700 });
var singleMullion = Product(100, -2000171, "Mullion", 500, 500, centreX: 0);
singleMullion.TypeId = 500;
foreach (var face in singleMullion.Geometry!.Faces)
face.RenderStyleId = 700;

var multiType = TypeElement(510, "MultiType", "DoorFamily");
multiType.CategoryId = DoorCategoryId;
var door = Product(110, DoorCategoryId, "Doors", 510, 510, centreX: 10);
door.TypeId = 510;
door.Geometry!.Faces[0].RenderStyleId = 710;
door.Geometry.Faces[1].RenderStyleId = 720;
for (int i = 2; i < door.Geometry.Faces.Count; i++)
door.Geometry.Faces[i].RenderStyleId = 710;

var hostWall = Product(999, WallCategoryId, "Walls", elemTypeParamId: null, nativeMasterSymbolId: null, centreX: 20);
return [steel, glass, wood, hostWall, singleType, singleMullion, multiType, door];
}

string baselineIfc = ExportToText(IfcProfile.Legacy, BuildBaseElements());

// Prepend extra elements and custom properties so STEP entity numbers of materials,
// material lists, products and types all shift significantly.
var extraColumnType = TypeElement(400, "ExtraColType", "ExtraFamily");
extraColumnType.CategoryId = -2001330;
var extraColumn = Product(50, -2001330, "Structural Columns", 400, 400, centreX: -20);
for (int p = 0; p < 12; p++)
{
extraColumn.Parameters.Add(new Parameter
{
Id = 800000 + p,
Definition = new Definition { Id = 800000 + p, Name = $"SpanExtra_{p}" },
StorageType = StorageType.Double,
Value = 10.0 + p,
});
}
string shiftedIfc = ExportToText(IfcProfile.Legacy, [extraColumnType, extraColumn, .. BuildBaseElements()]);

// Verify the relating IFCMATERIAL and IFCMATERIALLIST STEP numbers actually shifted.
int baseSteelEntity = EntityIds(baselineIfc, "IFCMATERIAL")
.Single(id => EntityArgs(baselineIfc, id, "IFCMATERIAL")[0] == "'Steel'");
int shiftedSteelEntity = EntityIds(shiftedIfc, "IFCMATERIAL")
.Single(id => EntityArgs(shiftedIfc, id, "IFCMATERIAL")[0] == "'Steel'");
Assert.NotEqual(baseSteelEntity, shiftedSteelEntity);

int baseMatList = SoleEntityId(baselineIfc, "IFCMATERIALLIST");
int shiftedMatList = SoleEntityId(shiftedIfc, "IFCMATERIALLIST");
Assert.NotEqual(baseMatList, shiftedMatList);

string expectedSingleProductGuid = $"'{IfcGuid.FromName("materials:700")}'";
string expectedSingleTypeGuid = $"'{IfcGuid.FromName("type-materials:700")}'";
string expectedMultiProductGuid = $"'{IfcGuid.FromName("materials:710,720")}'";
string expectedMultiTypeGuid = $"'{IfcGuid.FromName("type-materials:710,720")}'";

foreach ((string ifc, int steelEntity, int matListEntity) in new[]
{
(baselineIfc, baseSteelEntity, baseMatList),
(shiftedIfc, shiftedSteelEntity, shiftedMatList),
})
{
var rels = EntityIds(ifc, "IFCRELASSOCIATESMATERIAL")
.Select(id => EntityArgs(ifc, id, "IFCRELASSOCIATESMATERIAL")).ToList();
int memberTypeEntity = SoleEntityId(ifc, "IFCMEMBERTYPE");
int doorStyleEntity = SoleEntityId(ifc, "IFCDOORSTYLE");

var singleTypeRel = Assert.Single(rels, a => a[4] == $"(#{memberTypeEntity})" && a[5] == $"#{steelEntity}");
Assert.Equal(expectedSingleTypeGuid, singleTypeRel[0]);

var multiTypeRel = Assert.Single(rels, a => a[4] == $"(#{doorStyleEntity})" && a[5] == $"#{matListEntity}");
Assert.Equal(expectedMultiTypeGuid, multiTypeRel[0]);

Assert.Contains(rels, a => a[0] == expectedSingleProductGuid && a[5] == $"#{steelEntity}");
Assert.Contains(rels, a => a[0] == expectedMultiProductGuid && a[5] == $"#{matListEntity}");
}

// Current profile keeps its existing MD5("{project GlobalId}IfcRelAssociatesMaterial:{first related GlobalId}") seed.
string currentIfc = ExportToText(IfcProfile.Current, BuildBaseElements());
int projectEntity = SoleEntityId(currentIfc, "IFCPROJECT");
string projectGuid = EntityArgs(currentIfc, projectEntity, "IFCPROJECT")[0].Trim('\'');
int currentMemberType = SoleEntityId(currentIfc, "IFCMEMBERTYPE");
string currentMemberTypeGuid = EntityArgs(currentIfc, currentMemberType, "IFCMEMBERTYPE")[0].Trim('\'');
var currentRels = EntityIds(currentIfc, "IFCRELASSOCIATESMATERIAL")
.Select(id => EntityArgs(currentIfc, id, "IFCRELASSOCIATESMATERIAL")).ToList();
var mergedSteelRel = Assert.Single(currentRels, a => a[4].Contains($"#{currentMemberType}"));
Assert.Equal(
$"'{IfcGuid.FromMd5($"{projectGuid}IfcRelAssociatesMaterial:{currentMemberTypeGuid}")}'",
mergedSteelRel[0]);
}

[Theory]
Expand Down
Loading