Skip to content
Open
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
42 changes: 39 additions & 3 deletions src/NwcIO/Model/Clash/NwcClashBcfExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,51 @@ public static void Save(string path, NwcClashTest test, string? projectName = nu
/// </summary>
public static void Write(Stream stream, NwcClashTest test, string projectName = "Navisworks Clash Coordination")
{
ArgumentNullException.ThrowIfNull(stream);
ArgumentNullException.ThrowIfNull(test);
Write(stream, [test], projectName);
}

/// <summary>
/// Saves clash tests, including both standalone results and grouped clashes, to one BCF 2.1 archive file.
/// </summary>
public static void Save(string path, IEnumerable<NwcClashTest> tests, string projectName = "Navisworks Clash Coordination")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep existing calls with an empty collection source-compatible

Adding a second public collection overload makes this existing C# call ambiguous:

NwcClashBcfExporter.Save("empty.bcfzip", []);

A separate consumer project compiles against the current main/base but fails against this PR with CS0121: IEnumerable<NwcClashResult> versus IEnumerable<NwcClashTest>. The new Write overload similarly breaks Write(stream, []).

Please use a distinct method name for exporting a collection of tests, or an internal helper for the document/assembly/matrix callers, so existing public calls continue to compile. Add a consumer compilation check covering the empty collection.

{
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);
}

/// <summary>
/// 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.
/// </summary>
public static void Write(Stream stream, IEnumerable<NwcClashTest> 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");
Expand All @@ -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");
Expand Down
13 changes: 7 additions & 6 deletions src/NwcIO/Model/Clash/NwcClashMatrix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ public sealed class NwcClashMatrixCell
public NwcClashTest? ExecutedTest { get; set; }

public int ClashCount => ExecutedTest?.TotalClashCount ?? 0;
public IReadOnlyList<NwcClashResult> Clashes => ExecutedTest?.Results ?? (IReadOnlyList<NwcClashResult>)Array.Empty<NwcClashResult>();
public IReadOnlyList<NwcClashResult> Clashes =>
ExecutedTest?.AllClashes.ToList() ?? (IReadOnlyList<NwcClashResult>)Array.Empty<NwcClashResult>();

public NwcClashMatrixCell() { }

Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
}
2 changes: 1 addition & 1 deletion src/NwcIO/Model/NwcAssembly.cs
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ public void ExportClashReportXml(string path)
/// <summary>Exports detected clash results to a buildingSMART BCF 2.1 zip archive (.bcfzip).</summary>
public void ExportClashBcf(string path)
{
Clash.NwcClashBcfExporter.Save(path, ClashTests.SelectMany(t => t.Results), Name);
Clash.NwcClashBcfExporter.Save(path, ClashTests, Name);
}

/// <summary>Creates a Navisworks Federated Project Manifest (NWF) representation of this assembly.</summary>
Expand Down
6 changes: 2 additions & 4 deletions src/NwcIO/NwcDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}

/// <summary>Exports detected clash results to a BCF 2.1 stream.</summary>
Expand All @@ -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"));
}

/// <summary>Loads clash detective tests and results from a Navisworks Clash Report XML file.</summary>
Expand Down
89 changes: 89 additions & 0 deletions tests/NwcIO.Tests/ClashGroupingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading