diff --git a/src/NwcIO/Internal/Export/GlbExporter.cs b/src/NwcIO/Internal/Export/GlbExporter.cs index f08bd3d..89e99a9 100644 --- a/src/NwcIO/Internal/Export/GlbExporter.cs +++ b/src/NwcIO/Internal/Export/GlbExporter.cs @@ -56,11 +56,14 @@ public static void Write( var accessors = new List(); var gltfMeshes = new List(); - // 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(materials); var matIndexMap = new Dictionary(); - 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++) @@ -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, @@ -211,7 +217,7 @@ public static void Write( // 2. Build JSON chunk byte[] jsonData = BuildJsonChunk( - materials, + materialTable, gltfMeshes, accessors, bufferViews, @@ -251,7 +257,7 @@ public static void Write( } private static byte[] BuildJsonChunk( - IReadOnlyList materials, + List materials, List meshes, List accessors, List bufferViews, diff --git a/src/NwcIO/Model/NwcAssembly.cs b/src/NwcIO/Model/NwcAssembly.cs index dc0f66c..c562f6b 100644 --- a/src/NwcIO/Model/NwcAssembly.cs +++ b/src/NwcIO/Model/NwcAssembly.cs @@ -89,7 +89,10 @@ public static NwcAssembly Create(string name, IEnumerable models) var modelList = models.ToList(); var allMeshes = new List(); - var matMap = new Dictionary(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(); var rootChildren = new List(); var totalBox = BoundingBox3d.Empty; @@ -101,10 +104,7 @@ public static NwcAssembly Create(string name, IEnumerable 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) @@ -150,7 +150,7 @@ public static NwcAssembly Create(string name, IEnumerable 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); } /// diff --git a/tests/NwcIO.Tests/GlbExportTests.cs b/tests/NwcIO.Tests/GlbExportTests.cs index 4123a81..f2daca8 100644 --- a/tests/NwcIO.Tests/GlbExportTests.cs +++ b/tests/NwcIO.Tests/GlbExportTests.cs @@ -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); + } + } + } }