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,