Skip to content
Merged
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
44 changes: 39 additions & 5 deletions src/NwcIO/Model/Clash/NwcClashDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,18 @@ private static void RunCore(
string modelName)
{
bool sameSelection = ReferenceEquals(meshesA, meshesB) ||
(meshesA.Count == meshesB.Count && meshesA.Count > 0 && ReferenceEquals(meshesA[0], meshesB[0]));
meshesA.SequenceEqual(meshesB, ReferenceEqualityComparer.Instance);
Dictionary<NwcMesh, int>? firstIndexA = null;
HashSet<NwcMesh>? meshesInB = null;
if (!sameSelection)
{
firstIndexA = new Dictionary<NwcMesh, int>(ReferenceEqualityComparer.Instance);
for (int i = 0; i < meshesA.Count; i++)
{
firstIndexA.TryAdd(meshesA[i], i);
}
meshesInB = new HashSet<NwcMesh>(meshesB, ReferenceEqualityComparer.Instance);
}

bool ignoreSameLayer = test.Rules.Any(r => r.Enabled && r.RuleType == NwcClashRuleType.IgnoreSameLayer);
bool ignoreSameGroup = test.Rules.Any(r => r.Enabled && r.RuleType == NwcClashRuleType.IgnoreSameGroup);
Expand All @@ -74,12 +85,17 @@ private static void RunCore(
var meshA = meshesA[i];
var boxA = meshA.GetWorldBoundingBox();
if (boxA.IsEmpty) continue;
bool canVisitReverse = !sameSelection && meshesInB!.Contains(meshA);

int startJ = sameSelection ? i + 1 : 0;
for (int j = startJ; j < meshesB.Count; j++)
for (var j = sameSelection ? i + 1 : 0; j < meshesB.Count; j++)
{
var meshB = meshesB[j];
if (ReferenceEquals(meshA, meshB) || meshA.Id == meshB.Id) continue;
// The reverse pair was already visited from the earlier mesh in A.
if (canVisitReverse && firstIndexA!.TryGetValue(meshB, out int firstIndex) && firstIndex < i)
{
continue;
}

var boxB = meshB.GetWorldBoundingBox();
if (boxB.IsEmpty) continue;
Expand Down Expand Up @@ -149,7 +165,19 @@ private static void RunCoreMultiModel(
List<(NwcMesh Mesh, string ModelName, string? Layer)> itemsB,
NwcClashTest test)
{
bool sameSelection = itemsA.Select(item => item.Mesh).SequenceEqual(itemsB.Select(item => item.Mesh));
bool sameSelection = itemsA.Select(item => item.Mesh)
.SequenceEqual(itemsB.Select(item => item.Mesh), ReferenceEqualityComparer.Instance);
Dictionary<NwcMesh, int>? firstIndexA = null;
HashSet<NwcMesh>? meshesInB = null;
if (!sameSelection)
{
firstIndexA = new Dictionary<NwcMesh, int>(ReferenceEqualityComparer.Instance);
for (int i = 0; i < itemsA.Count; i++)
{
firstIndexA.TryAdd(itemsA[i].Mesh, i);
}
meshesInB = new HashSet<NwcMesh>(itemsB.Select(item => item.Mesh), ReferenceEqualityComparer.Instance);
}
bool ignoreSameFile = test.Rules.Any(r => r.Enabled && r.RuleType == NwcClashRuleType.IgnoreSameFile);
bool ignoreSameLayer = test.Rules.Any(r => r.Enabled && r.RuleType == NwcClashRuleType.IgnoreSameLayer);

Expand All @@ -158,11 +186,17 @@ private static void RunCoreMultiModel(
var itemA = itemsA[i];
var boxA = itemA.Mesh.GetWorldBoundingBox();
if (boxA.IsEmpty) continue;
bool canVisitReverse = !sameSelection && meshesInB!.Contains(itemA.Mesh);

for (int j = sameSelection ? i + 1 : 0; j < itemsB.Count; j++)
for (var j = sameSelection ? i + 1 : 0; j < itemsB.Count; j++)
{
var itemB = itemsB[j];
if (ReferenceEquals(itemA.Mesh, itemB.Mesh)) continue;
// Mesh IDs can repeat across models; only references identify pairs.
if (canVisitReverse && firstIndexA!.TryGetValue(itemB.Mesh, out int firstIndex) && firstIndex < i)
{
continue;
}

if (ignoreSameFile && string.Equals(itemA.ModelName, itemB.ModelName, StringComparison.OrdinalIgnoreCase))
{
Expand Down
54 changes: 54 additions & 0 deletions tests/NwcIO.Tests/ClashDetectiveTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,60 @@ public void MeshClashDetectionIdentifiesInterpenetratingElements()
Assert.NotNull(clash.Item2.IfcGuid);
}

[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(true, true)]
public void ClashDetection_OverlappingSelectionsReportsEachPairOnce(bool federated, bool includeSharedPair)
{
var mesh1 = CreateBoxMesh(1, new(0, 0, 0), new(2, 2, 2));
var mesh2 = CreateBoxMesh(2, new(1.5, 0.5, 0.5), new(3.5, 1.5, 1.5));
var mesh3 = CreateBoxMesh(3, new(3, 0, 0), new(5, 2, 2));
var builder = new NwcDocumentBuilder().SetFileInfo("Architecture.nwc")
.AddMesh(mesh1)
.AddMesh(mesh2);
if (!federated)
{
builder.AddMesh(mesh3);
}
var doc = builder.Build();
var test = new NwcClashTest("Overlapping selections");
test.SelectionA.ExplicitNodeIds.AddRange([1, 2]);
test.SelectionB.ExplicitNodeIds.AddRange(includeSharedPair ? [1, 2, 3] : [1, 3]);

if (federated)
{
var otherDoc = new NwcDocumentBuilder().SetFileInfo("MEP.nwc").AddMesh(mesh3).Build();
NwcClashDetector.Run(NwcAssembly.Create("Federated", doc, otherDoc), test);
}
else
{
doc.RunClashDetection(test);
}

var pairs = test.Results.Select(clash => (
Math.Min(clash.Item1.Mesh!.Id, clash.Item2.Mesh!.Id),
Math.Max(clash.Item1.Mesh!.Id, clash.Item2.Mesh!.Id))).OrderBy(pair => pair.Item1).ToArray();
Assert.Equal([(1, 2), (2, 3)], pairs);
}

[Fact]
public void AssemblyClashDetection_AllVsAllPreservesPairsWithRepeatedMeshIds()
{
var architecture = new NwcDocumentBuilder().SetFileInfo("Architecture.nwc")
.AddMesh(CreateBoxMesh(1, new(0, 0, 0), new(2, 2, 2)))
.Build();
var mep = new NwcDocumentBuilder().SetFileInfo("MEP.nwc")
.AddMesh(CreateBoxMesh(1, new(1, 1, 1), new(3, 3, 3)))
.Build();

var test = NwcAssembly.Create("Federated", architecture, mep).RunClashDetection();

var clash = Assert.Single(test.Results);
Assert.NotEqual(clash.Item1.ModelName, clash.Item2.ModelName);
}

[Theory]
[InlineData(false, false, 0)]
[InlineData(true, false, 2)]
Expand Down
Loading