diff --git a/src/NwcIO/Model/Clash/NwcClashDetector.cs b/src/NwcIO/Model/Clash/NwcClashDetector.cs
index 5b31f72..7e61034 100644
--- a/src/NwcIO/Model/Clash/NwcClashDetector.cs
+++ b/src/NwcIO/Model/Clash/NwcClashDetector.cs
@@ -12,10 +12,12 @@ public static class NwcClashDetector
///
/// Executes clash detection across an individual Navisworks document according to the specified test configuration.
///
+ /// A selection names a set that the document doesn't contain.
public static NwcClashTest Run(NwcDocument document, NwcClashTest test)
{
ArgumentNullException.ThrowIfNull(document);
ArgumentNullException.ThrowIfNull(test);
+ ThrowIfSetIsMissing([document], test);
var meshesA = ResolveSelection(document, test.SelectionA);
var meshesB = ResolveSelection(document, test.SelectionB);
@@ -27,10 +29,12 @@ public static NwcClashTest Run(NwcDocument document, NwcClashTest test)
///
/// Executes clash detection across all contributing models of a federated Navisworks assembly.
///
+ /// A selection names a set that none of the models contains.
public static NwcClashTest Run(NwcAssembly assembly, NwcClashTest test)
{
ArgumentNullException.ThrowIfNull(assembly);
ArgumentNullException.ThrowIfNull(test);
+ ThrowIfSetIsMissing(assembly.Models, test);
var itemsA = new List<(NwcMesh Mesh, string ModelName, string? Layer)>();
var itemsB = new List<(NwcMesh Mesh, string ModelName, string? Layer)>();
@@ -350,9 +354,29 @@ private static IReadOnlyList ResolveSelection(NwcDocument document, Nwc
matchedMeshes.AddRange(document.Meshes.Where(m => m.Node is not null && nodeSet.Contains(m.Node)));
}
}
- if (matchedMeshes.Count > 0) return matchedMeshes;
+ // Only the named sets, even when they match nothing: a model without them adds no meshes
+ return matchedMeshes;
}
return document.Meshes;
}
+
+ ///
+ /// Throws when a set named by a selection is in none of the models, so a misspelled name can't run against
+ /// nothing without notice. A set that exists in only some models of an assembly is fine.
+ ///
+ private static void ThrowIfSetIsMissing(IReadOnlyList documents, NwcClashTest test)
+ {
+ foreach (var selection in new[] { test.SelectionA, test.SelectionB })
+ {
+ foreach (string name in selection.SetNames)
+ {
+ if (!documents.Any(d => d.FindSet(name) is not null))
+ {
+ throw new ArgumentException(
+ $"Clash test '{test.Name}' selects the set '{name}', which no model contains.", nameof(test));
+ }
+ }
+ }
+ }
}
diff --git a/tests/NwcIO.Tests/ClashDetectiveTests.cs b/tests/NwcIO.Tests/ClashDetectiveTests.cs
index 762407d..b69ede0 100644
--- a/tests/NwcIO.Tests/ClashDetectiveTests.cs
+++ b/tests/NwcIO.Tests/ClashDetectiveTests.cs
@@ -104,6 +104,78 @@ public void AssemblyClashDetection_ReportsOnlyUniqueCrossModelPairs(bool interse
Assert.All(test.Results, clash => Assert.NotEqual(clash.Item1.ModelName, clash.Item2.ModelName));
}
+ private static NwcDocument CreateClashingDocument(string name, params Model.Sets.NwcSetItem[] sets)
+ {
+ var builder = new NwcDocumentBuilder().SetFileInfo(name)
+ .AddMesh(CreateBoxMesh(1, new(0, 0, 0), new(2, 2, 2)))
+ .AddMesh(CreateBoxMesh(2, new(1, 0, 0), new(3, 2, 2)));
+ foreach (var set in sets)
+ {
+ builder.AddSelectionSet(set);
+ }
+
+ return builder.Build();
+ }
+
+ [Fact]
+ public void SetSelection_UnknownSetName_Throws()
+ {
+ var doc = CreateClashingDocument("Model.nwc");
+ var test = new NwcClashTest("Typo", NwcClashTestType.Hard, tolerance: 0.01)
+ {
+ SelectionA = NwcClashSelection.FromSet("NoSuchSet"),
+ SelectionB = NwcClashSelection.All(),
+ };
+
+ var ex = Assert.Throws(() => NwcClashDetector.Run(doc, test));
+ Assert.Contains("NoSuchSet", ex.Message, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void SetSelection_SetWithoutMeshes_SelectsNothing()
+ {
+ var doc = CreateClashingDocument("Model.nwc", new Model.Sets.NwcSelectionSet("Empty", [999]));
+ var test = new NwcClashTest("Empty set", NwcClashTestType.Hard, tolerance: 0.01)
+ {
+ SelectionA = NwcClashSelection.FromSet("Empty"),
+ SelectionB = NwcClashSelection.All(),
+ };
+
+ NwcClashDetector.Run(doc, test);
+
+ Assert.Equal(0, test.TotalClashCount);
+ }
+
+ [Fact]
+ public void SetSelection_SetInOneModelOnly_SelectsNothingFromTheOthers()
+ {
+ var architecture = CreateClashingDocument("Architecture.nwc", new Model.Sets.NwcSelectionSet("Empty", [999]));
+ var mep = CreateClashingDocument("MEP.nwc");
+ var test = new NwcClashTest("Federated", NwcClashTestType.Hard, tolerance: 0.01)
+ {
+ SelectionA = NwcClashSelection.FromSet("Empty"),
+ SelectionB = NwcClashSelection.All(),
+ };
+
+ NwcClashDetector.Run(NwcAssembly.Create("Federated", architecture, mep), test);
+
+ Assert.Equal(0, test.TotalClashCount);
+ }
+
+ [Fact]
+ public void SetSelection_SetMissingFromEveryModel_Throws()
+ {
+ var assembly = NwcAssembly.Create("Federated", CreateClashingDocument("A.nwc"), CreateClashingDocument("B.nwc"));
+ var test = new NwcClashTest("Typo", NwcClashTestType.Hard, tolerance: 0.01)
+ {
+ SelectionA = NwcClashSelection.All(),
+ SelectionB = NwcClashSelection.FromSet("NoSuchSet"),
+ };
+
+ var ex = Assert.Throws(() => NwcClashDetector.Run(assembly, test));
+ Assert.Contains("NoSuchSet", ex.Message, StringComparison.Ordinal);
+ }
+
[Fact]
public void IgnoreSameLayerRuleFiltersOutSameLayerClashes()
{