From 5bba6fae56ad8c8124506bcb6e00ae358aa612de Mon Sep 17 00:00:00 2001 From: menes97 <38889017+menes97@users.noreply.github.com> Date: Thu, 12 Nov 2026 22:13:06 +0300 Subject: [PATCH] Clash sets: select only the named sets, and reject a set no model has ResolveSelection returned every mesh of the document when the named sets resolved to nothing, so a misspelled set name, a set without meshes, or a set present in only one model of an assembly ran against the whole model without notice. The set branch now returns its matches even when they are empty, like the node id and filter branches, and Run throws an ArgumentException naming a set that none of the models contains. Co-Authored-By: Claude Opus 5.5 --- src/NwcIO/Model/Clash/NwcClashDetector.cs | 26 +++++++- tests/NwcIO.Tests/ClashDetectiveTests.cs | 72 +++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) 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() {