From fcb56a693079af55055ddab34fb5f94cc6b0010a Mon Sep 17 00:00:00 2001 From: menes97 <38889017+menes97@users.noreply.github.com> Date: Thu, 12 Nov 2026 22:17:15 +0300 Subject: [PATCH] Include grouped clashes in BCF exports and the clash matrix Grouping moves clashes from Results into Groups, but ExportClashBcf on documents and assemblies, the matrix's SaveBcf, SaveJson and NwcClashMatrixCell.Clashes read only Results, so grouped clashes were left out without notice, also through the CLI's --clash-group with --clash-bcf. The BCF paths now go through new NwcClashBcfExporter.Save/Write overloads for several tests, which write a topic per group and per ungrouped clash into one archive, like the existing single-test overload that now uses them. The matrix lists AllClashes. Co-Authored-By: Claude Opus 5.5 --- src/NwcIO/Model/Clash/NwcClashBcfExporter.cs | 42 ++++++++- src/NwcIO/Model/Clash/NwcClashMatrix.cs | 13 +-- src/NwcIO/Model/NwcAssembly.cs | 2 +- src/NwcIO/NwcDocument.cs | 6 +- tests/NwcIO.Tests/ClashGroupingTests.cs | 89 ++++++++++++++++++++ 5 files changed, 138 insertions(+), 14 deletions(-) diff --git a/src/NwcIO/Model/Clash/NwcClashBcfExporter.cs b/src/NwcIO/Model/Clash/NwcClashBcfExporter.cs index c9817c9..84fe8fe 100644 --- a/src/NwcIO/Model/Clash/NwcClashBcfExporter.cs +++ b/src/NwcIO/Model/Clash/NwcClashBcfExporter.cs @@ -60,15 +60,51 @@ public static void Save(string path, NwcClashTest test, string? projectName = nu /// public static void Write(Stream stream, NwcClashTest test, string projectName = "Navisworks Clash Coordination") { - ArgumentNullException.ThrowIfNull(stream); ArgumentNullException.ThrowIfNull(test); + Write(stream, [test], projectName); + } + + /// + /// Saves clash tests, including both standalone results and grouped clashes, to one BCF 2.1 archive file. + /// + public static void Save(string path, IEnumerable tests, string projectName = "Navisworks Clash Coordination") + { + ArgumentNullException.ThrowIfNull(path); + ArgumentNullException.ThrowIfNull(tests); + + string? dir = Path.GetDirectoryName(Path.GetFullPath(path)); + if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + + using var fs = File.Create(path); + Write(fs, tests, projectName); + } + + /// + /// Writes clash tests, including both standalone results and grouped clashes, to one BCF 2.1 zip stream: + /// a topic per clash group and a topic per clash that is in no group. + /// + public static void Write(Stream stream, IEnumerable tests, string projectName = "Navisworks Clash Coordination") + { + ArgumentNullException.ThrowIfNull(stream); + ArgumentNullException.ThrowIfNull(tests); using var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true); // 1. Root bcf.version WriteBcfVersion(archive); - // 2. Export Clash Groups as unified topics + foreach (var test in tests) + { + WriteTest(archive, test, projectName); + } + } + + private static void WriteTest(ZipArchive archive, NwcClashTest test, string projectName) + { + // Export Clash Groups as unified topics foreach (var group in test.Groups) { string topicFolder = group.GroupGuid.ToString("D"); @@ -88,7 +124,7 @@ public static void Write(Stream stream, NwcClashTest test, string projectName = } } - // 3. Export standalone clash results + // Export standalone clash results foreach (var clash in test.Results) { string topicFolder = clash.ResultGuid.ToString("D"); diff --git a/src/NwcIO/Model/Clash/NwcClashMatrix.cs b/src/NwcIO/Model/Clash/NwcClashMatrix.cs index 02720ab..96a2a33 100644 --- a/src/NwcIO/Model/Clash/NwcClashMatrix.cs +++ b/src/NwcIO/Model/Clash/NwcClashMatrix.cs @@ -61,7 +61,8 @@ public sealed class NwcClashMatrixCell public NwcClashTest? ExecutedTest { get; set; } public int ClashCount => ExecutedTest?.TotalClashCount ?? 0; - public IReadOnlyList Clashes => ExecutedTest?.Results ?? (IReadOnlyList)Array.Empty(); + public IReadOnlyList Clashes => + ExecutedTest?.AllClashes.ToList() ?? (IReadOnlyList)Array.Empty(); public NwcClashMatrixCell() { } @@ -335,10 +336,10 @@ public void SaveJson(string filePath, bool indent = true) writer.WriteNumber("tolerance", c.Tolerance); writer.WriteNumber("clashesCount", c.ClashCount); - if (c.ExecutedTest is not null && c.ExecutedTest.Results.Count > 0) + if (c.ExecutedTest is not null && c.ExecutedTest.TotalClashCount > 0) { writer.WriteStartArray("clashes"); - foreach (var clash in c.ExecutedTest.Results) + foreach (var clash in c.ExecutedTest.AllClashes) { writer.WriteStartObject(); writer.WriteString("name", clash.Name); @@ -365,11 +366,11 @@ public void SaveJson(string filePath, bool indent = true) public void SaveBcf(string filePath) { ArgumentNullException.ThrowIfNull(filePath); - var allClashes = Cells + var tests = Cells .Where(c => c.ExecutedTest is not null) - .SelectMany(c => c.ExecutedTest!.Results) + .Select(c => c.ExecutedTest!) .ToList(); - NwcClashBcfExporter.Save(filePath, allClashes, Name); + NwcClashBcfExporter.Save(filePath, tests, Name); } } diff --git a/src/NwcIO/Model/NwcAssembly.cs b/src/NwcIO/Model/NwcAssembly.cs index c562f6b..071d1dc 100644 --- a/src/NwcIO/Model/NwcAssembly.cs +++ b/src/NwcIO/Model/NwcAssembly.cs @@ -344,7 +344,7 @@ public void ExportClashReportXml(string path) /// Exports detected clash results to a buildingSMART BCF 2.1 zip archive (.bcfzip). public void ExportClashBcf(string path) { - Clash.NwcClashBcfExporter.Save(path, ClashTests.SelectMany(t => t.Results), Name); + Clash.NwcClashBcfExporter.Save(path, ClashTests, Name); } /// Creates a Navisworks Federated Project Manifest (NWF) representation of this assembly. diff --git a/src/NwcIO/NwcDocument.cs b/src/NwcIO/NwcDocument.cs index 124ec96..1f38945 100644 --- a/src/NwcIO/NwcDocument.cs +++ b/src/NwcIO/NwcDocument.cs @@ -452,8 +452,7 @@ public void ExportClashBcf(string path, string? testName = null) var tests = string.IsNullOrEmpty(testName) ? ClashTests : ClashTests.Where(t => string.Equals(t.Name, testName, StringComparison.OrdinalIgnoreCase)); - var clashes = tests.SelectMany(t => t.Results); - Model.Clash.NwcClashBcfExporter.Save(path, clashes, Path.GetFileNameWithoutExtension(FileInfo.SourcePath ?? "Model")); + Model.Clash.NwcClashBcfExporter.Save(path, tests, Path.GetFileNameWithoutExtension(FileInfo.SourcePath ?? "Model")); } /// Exports detected clash results to a BCF 2.1 stream. @@ -462,8 +461,7 @@ public void ExportClashBcf(Stream stream, string? testName = null) var tests = string.IsNullOrEmpty(testName) ? ClashTests : ClashTests.Where(t => string.Equals(t.Name, testName, StringComparison.OrdinalIgnoreCase)); - var clashes = tests.SelectMany(t => t.Results); - Model.Clash.NwcClashBcfExporter.Write(stream, clashes, Path.GetFileNameWithoutExtension(FileInfo.SourcePath ?? "Model")); + Model.Clash.NwcClashBcfExporter.Write(stream, tests, Path.GetFileNameWithoutExtension(FileInfo.SourcePath ?? "Model")); } /// Loads clash detective tests and results from a Navisworks Clash Report XML file. diff --git a/tests/NwcIO.Tests/ClashGroupingTests.cs b/tests/NwcIO.Tests/ClashGroupingTests.cs index d2e7e3e..daa6a81 100644 --- a/tests/NwcIO.Tests/ClashGroupingTests.cs +++ b/tests/NwcIO.Tests/ClashGroupingTests.cs @@ -220,6 +220,95 @@ public void NwcClashBcfExporter_ExportsGroupedClashesToBcfZip() Assert.NotNull(viewpointEntry); } + private static NwcClashTest CreateGroupedTest() + { + var test = new NwcClashTest("Grouped"); + test.Results.AddRange( + [ + CreateClash("c1", new Vector3d(0, 0, 0), -0.05), + CreateClash("c2", new Vector3d(0.2, 0, 0), -0.05), + CreateClash("c3", new Vector3d(50, 0, 0), -0.05), + CreateClash("c4", new Vector3d(50.3, 0, 0), -0.05), + CreateClash("c5", new Vector3d(200, 0, 0), -0.05), + ]); + NwcClashGrouper.GroupByProximity(test, clusterDistance: 2.0, minClusterSize: 2); + Assert.Equal(2, test.Groups.Count); + Assert.Single(test.Results); + return test; + } + + private static int CountTopics(Stream bcf) + { + bcf.Position = 0; + using var zip = new ZipArchive(bcf, ZipArchiveMode.Read, leaveOpen: true); + return zip.Entries.Count(e => e.FullName.EndsWith("/markup.bcf", StringComparison.Ordinal)); + } + + [Fact] + public void Document_ExportClashBcf_IncludesGroupedClashes() + { + var doc = new NwcDocumentBuilder().SetFileInfo("Model.nwc").Build(); + doc.ClashTests.Add(CreateGroupedTest()); + + using var ms = new MemoryStream(); + doc.ExportClashBcf(ms); + + Assert.Equal(3, CountTopics(ms)); // 2 groups + 1 ungrouped clash + } + + [Fact] + public void Assembly_And_Matrix_ExportClashBcf_IncludeGroupedClashes() + { + var assembly = NwcAssembly.Create("Asm"); + assembly.ClashTests.Add(CreateGroupedTest()); + var matrix = new NwcClashMatrix("Matrix"); + matrix.Cells.Add(new NwcClashMatrixCell("A", "B") { ExecutedTest = CreateGroupedTest() }); + + string assemblyPath = Path.Combine(Path.GetTempPath(), $"nwcio_{Guid.NewGuid():N}.bcfzip"); + string matrixPath = Path.Combine(Path.GetTempPath(), $"nwcio_{Guid.NewGuid():N}.bcfzip"); + try + { + assembly.ExportClashBcf(assemblyPath); + matrix.SaveBcf(matrixPath); + + using (var fs = File.OpenRead(assemblyPath)) Assert.Equal(3, CountTopics(fs)); + using (var fs = File.OpenRead(matrixPath)) Assert.Equal(3, CountTopics(fs)); + } + finally + { + File.Delete(assemblyPath); + File.Delete(matrixPath); + } + } + + [Fact] + public void Matrix_ClashesAndJson_IncludeGroupedClashes() + { + var cell = new NwcClashMatrixCell("A", "B") { ExecutedTest = CreateGroupedTest() }; + var matrix = new NwcClashMatrix("Matrix"); + matrix.Cells.Add(cell); + + Assert.Equal(5, cell.Clashes.Count); + + string path = Path.Combine(Path.GetTempPath(), $"nwcio_{Guid.NewGuid():N}.json"); + try + { + matrix.SaveJson(path); + using var json = System.Text.Json.JsonDocument.Parse(File.ReadAllText(path)); + var written = json.RootElement.EnumerateObject() + .Select(p => p.Value) + .Where(v => v.ValueKind == System.Text.Json.JsonValueKind.Array) + .SelectMany(a => a.EnumerateArray()) + .Single(c => c.TryGetProperty("clashesCount", out _)); + Assert.Equal(5, written.GetProperty("clashesCount").GetInt32()); + Assert.Equal(5, written.GetProperty("clashes").GetArrayLength()); + } + finally + { + File.Delete(path); + } + } + private static NwcClashResult CreateClash( string name, Vector3d point,