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
26 changes: 16 additions & 10 deletions src/NwcIO/Internal/Export/GlbExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,14 @@ public static void Write(
var accessors = new List<AccessorDef>();
var gltfMeshes = new List<MeshDef>();

// Material indexing
// Material indexing. Meshes may reference materials the caller did not list
// (for example an assembly material table deduped by name), so the table is
// grown on demand instead of falling back to an arbitrary index 0.
var materialTable = new List<NwcMaterial>(materials);
var matIndexMap = new Dictionary<NwcMaterial, int>();
for (int i = 0; i < materials.Count; i++)
for (int i = 0; i < materialTable.Count; i++)
{
matIndexMap[materials[i]] = i;
matIndexMap[materialTable[i]] = i;
}

for (int m = 0; m < meshes.Count; m++)
Expand Down Expand Up @@ -188,14 +191,17 @@ public static void Write(

// Material assignment
int? matIdx = null;
if (mesh.Material is not null && matIndexMap.TryGetValue(mesh.Material, out int foundIdx))
if (mesh.Material is not null)
{
if (!matIndexMap.TryGetValue(mesh.Material, out int foundIdx))
{
foundIdx = materialTable.Count;
materialTable.Add(mesh.Material);
matIndexMap[mesh.Material] = foundIdx;
}

matIdx = foundIdx;
}
else if (materials.Count > 0)
{
matIdx = 0;
}

gltfMeshes.Add(new MeshDef(
mesh.Name,
Expand All @@ -211,7 +217,7 @@ public static void Write(

// 2. Build JSON chunk
byte[] jsonData = BuildJsonChunk(
materials,
materialTable,
gltfMeshes,
accessors,
bufferViews,
Expand Down Expand Up @@ -251,7 +257,7 @@ public static void Write(
}

private static byte[] BuildJsonChunk(
IReadOnlyList<NwcMaterial> materials,
List<NwcMaterial> materials,
List<MeshDef> meshes,
List<AccessorDef> accessors,
List<BufferViewDef> bufferViews,
Expand Down
12 changes: 6 additions & 6 deletions src/NwcIO/Model/NwcAssembly.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ public static NwcAssembly Create(string name, IEnumerable<NwcDocument> models)

var modelList = models.ToList();
var allMeshes = new List<NwcMesh>();
var matMap = new Dictionary<string, NwcMaterial>(StringComparer.OrdinalIgnoreCase);
// Dedupe by reference, NOT by name: disciplines routinely define their own
// material with a shared name (Concrete, Steel) and different colours, and a
// by-name map would silently drop the later ones from AllMaterials.
var matSet = new HashSet<NwcMaterial>();

var rootChildren = new List<NwcNode>();
var totalBox = BoundingBox3d.Empty;
Expand All @@ -101,10 +104,7 @@ public static NwcAssembly Create(string name, IEnumerable<NwcDocument> models)

foreach (var mat in doc.Materials)
{
if (!matMap.ContainsKey(mat.Name))
{
matMap[mat.Name] = mat;
}
matSet.Add(mat);
}

foreach (var mesh in doc.Meshes)
Expand Down Expand Up @@ -150,7 +150,7 @@ public static NwcAssembly Create(string name, IEnumerable<NwcDocument> models)
};
rootNode.Children.AddRange(rootChildren);

return new NwcAssembly(name, modelList, rootNode, allMeshes, matMap.Values.ToList(), totalBox, allViewpoints);
return new NwcAssembly(name, modelList, rootNode, allMeshes, [.. matSet], totalBox, allViewpoints);
}

/// <summary>
Expand Down
70 changes: 70 additions & 0 deletions tests/NwcIO.Tests/GlbExportTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,74 @@ public void GlbExporter_SingleMesh_ProducesValidGlbContainer()
Assert.True(binChunkLength % 4 == 0, "BIN chunk must be 4-byte aligned");
Assert.Equal((uint)(glb.Length - (binChunkOffset + 8)), binChunkLength);
}

[Fact]
public void AssemblyGlb_SameNamedMaterialsInDifferentModels_KeepTheirOwnColors()
{
// Two models, each with its OWN material instance named "Concrete" but a
// different colour: grey in A, blue in B. The by-name assembly dedupe used to
// drop B's material, silently recolouring B's mesh with material index 0.
static NwcDocument DocWith(string source, string meshName, float r, float g, float b)
{
var material = new NwcMaterial { Id = 1, Name = "Concrete", Color = new RgbaColor(r, g, b) };
return new NwcDocumentBuilder().SetFileInfo(source)
.AddMaterial(material)
.AddMesh(new NwcMesh
{
Id = 1,
Name = meshName,
Vertices = [new(0, 0, 0), new(1, 0, 0), new(0, 1, 0)],
Indices = [0, 1, 2],
BoundingBox = new BoundingBox3d(new(0, 0, 0), new(1, 1, 0)),
Material = material,
})
.Build();
}

var assembly = NwcAssembly.Create("X", DocWith("A.rvt", "MeshA", 128, 128, 128), DocWith("B.rvt", "MeshB", 0, 0, 255));

// Both distinct material instances must survive the assembly material table.
Assert.Equal(2, assembly.AllMaterials.Count);

using var ms = new MemoryStream();
GlbExporter.Write(assembly.AllMeshes, assembly.AllMaterials, ms, worldCoordinates: false);
byte[] glb = ms.ToArray();

uint jsonChunkLength = BinaryPrimitives.ReadUInt32LittleEndian(glb.AsSpan(12));
string json = System.Text.Encoding.UTF8.GetString(glb.AsSpan(20, (int)jsonChunkLength).ToArray()).Trim();

using var jsonDoc = JsonDocument.Parse(json);
var root = jsonDoc.RootElement;

// Two materials exported: one grey, one blue.
var materials = root.GetProperty("materials");
Assert.Equal(2, materials.GetArrayLength());
int blueIndex = -1, greyIndex = -1;
for (int i = 0; i < 2; i++)
{
float r = materials[i].GetProperty("pbrMetallicRoughness").GetProperty("baseColorFactor")[0].GetSingle();
float b = materials[i].GetProperty("pbrMetallicRoughness").GetProperty("baseColorFactor")[2].GetSingle();
if (b > 0.9f && r < 0.1f) blueIndex = i;
if (r > 0.4f && b > 0.4f) greyIndex = i;
}
Assert.True(blueIndex >= 0, "blue Concrete missing from the materials array");
Assert.True(greyIndex >= 0, "grey Concrete missing from the materials array");

// Each mesh references its own material: MeshA grey, MeshB blue.
var meshes = root.GetProperty("meshes");
Assert.Equal(2, meshes.GetArrayLength());
foreach (var mesh in meshes.EnumerateArray())
{
int matIdx = mesh.GetProperty("primitives")[0].GetProperty("material").GetInt32();
string? meshName = mesh.GetProperty("name").GetString();
if (meshName == "MeshA")
{
Assert.Equal(greyIndex, matIdx);
}
else if (meshName == "MeshB")
{
Assert.Equal(blueIndex, matIdx);
}
}
}
}
Loading