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,