diff --git a/.github/workflows/merge-and-run.yaml b/.github/workflows/merge-and-run.yaml index 5ae2d7b2..2f44b7ff 100644 --- a/.github/workflows/merge-and-run.yaml +++ b/.github/workflows/merge-and-run.yaml @@ -26,6 +26,13 @@ jobs: lfs: ${{ inputs.lfs }} fetch-depth: 0 # fetches the whole history in order to be able to merge with the base branch + - name: Setup DotNet Environment + uses: actions/setup-dotnet@v1 + with: + dotnet-version: | + 6.0.x + 8.0.x + - name: Setup Git VIM Robot User Info run: | git config --global user.email "vim-robot@email.com" diff --git a/src/cs/vim/Vim.Format.Tests/RoomServiceTests.cs b/src/cs/vim/Vim.Format.Tests/RoomServiceTests.cs new file mode 100644 index 00000000..8359ca8e --- /dev/null +++ b/src/cs/vim/Vim.Format.Tests/RoomServiceTests.cs @@ -0,0 +1,67 @@ +using NUnit.Framework; +using System.IO; +using System.Linq; +using Vim.Format.Merge; +using Vim.Format.ObjectModel; +using Vim.Format.SceneBuilder; +using Vim.Math3d; +using Vim.Util.Tests; + +namespace Vim.Format.Tests; + +[TestFixture] +public static class RoomServiceTests +{ + /// + /// Merges two unrelated VIM files and then calculates the inclusion of elements + /// coming from the first VIM file (containing no rooms) in the rooms of the second VIM file. + /// + [Test] + public static void TestRoomService_ComputeElementsInRoom() + { + var ctx = new CallerTestContext(); + var dir = ctx.PrepareDirectory(); + + // Merge the VIM files. + var vim1 = Path.Combine(VimFormatRepoPaths.DataDir, "Wolford_Residence.r2023.om_v5.0.0.vim"); + var vim2 = Path.Combine(VimFormatRepoPaths.DataDir, "RoomTest.vim"); + var mergedFilePath = Path.Combine(dir, "merged.vim"); + + var mergeFiles = new MergeConfigFiles(new[] + { + (vim1, Matrix4x4.Identity), + (vim2, Matrix4x4.Identity) + }, mergedFilePath); + + var mergeOptions = new MergeConfigOptions() + { + GeneratorString = nameof(TestRoomService_ComputeElementsInRoom), + VersionString = "0.0.0", + }; + + MergeService.MergeVimFiles(mergeFiles, mergeOptions); + + var mergedVim = VimScene.LoadVim(mergedFilePath); + Assert.DoesNotThrow(() => mergedVim.Validate()); + + var dm = mergedVim.DocumentModel; + + var elementInfos = + Enumerable.Range(0, mergedVim.DocumentModel.NumElement) + .AsParallel() + .Select(elementIndex => dm.GetElementInfo(elementIndex)) + .ToArray(); + + var wolfordFamilyInstances = elementInfos + .Where(ei => ei.BimDocumentFileName.Contains("Wolford") && ei.IsFamilyInstance) + .ToArray(); + var wolfordFamilyInstanceElementIndices = wolfordFamilyInstances.Select(ei => ei.ElementIndex).ToHashSet(); + + // Compute the element association in each room. + var familyInstancesInRooms = RoomService.ComputeElementsInRoom( + mergedVim, + ei => wolfordFamilyInstanceElementIndices.Contains(ei.ElementIndex)); + + Assert.IsNotEmpty(familyInstancesInRooms); + } +} diff --git a/src/cs/vim/Vim.Format/ObjectModel/ElementInfo.cs b/src/cs/vim/Vim.Format/ObjectModel/ElementInfo.cs index f8f40bac..ad545f29 100644 --- a/src/cs/vim/Vim.Format/ObjectModel/ElementInfo.cs +++ b/src/cs/vim/Vim.Format/ObjectModel/ElementInfo.cs @@ -86,6 +86,8 @@ public bool IsSystem public int CategoryIndex => DocumentModel.GetElementCategoryIndex(ElementIndex); public int LevelIndex => DocumentModel.GetElementLevelIndex(ElementIndex); public int LevelElementIndex => DocumentModel.GetLevelElementIndex(LevelIndex); + public int RoomIndex => DocumentModel.GetElementRoomIndex(ElementIndex); + public int RoomElementIndex => DocumentModel.GetRoomElementIndex(RoomIndex); public int BimDocumentIndex => DocumentModel.GetElementBimDocumentIndex(ElementIndex); public int WorksetIndex => DocumentModel.GetElementWorksetIndex(ElementIndex); public int FamilyInstanceElementIndex => DocumentModel.GetFamilyInstanceElementIndex(FamilyInstanceIndex); @@ -100,6 +102,7 @@ public bool IsSystem public Element Element => DocumentModel.ElementList.ElementAtOrDefault(ElementIndex); public Category Category => DocumentModel.CategoryList.ElementAtOrDefault(CategoryIndex); public Level Level => DocumentModel.LevelList.ElementAtOrDefault(LevelIndex); + public Room Room => DocumentModel.RoomList.ElementAtOrDefault(RoomIndex); public BimDocument BimDocument => DocumentModel.BimDocumentList.ElementAtOrDefault(BimDocumentIndex); public Workset Workset => DocumentModel.WorksetList.ElementAtOrDefault(WorksetIndex); public FamilyInstance FamilyInstance => DocumentModel.FamilyInstanceList.ElementAtOrDefault(FamilyInstanceIndex); @@ -164,6 +167,7 @@ public string ElementUniqueIdWithBimScopedElementIdFallback public string ElementName => DocumentModel.GetElementName(ElementIndex); public string CategoryName => DocumentModel.GetCategoryName(CategoryIndex); + public string CategoryBuiltInName => DocumentModel.GetCategoryBuiltInCategory(CategoryIndex); public string LevelName => DocumentModel.GetElementName(LevelElementIndex); public string FamilyName => DocumentModel.GetElementName(FamilyElementIndex, DocumentModel.GetElementFamilyName(ElementIndex)); public string FamilyTypeName => DocumentModel.GetElementName(FamilyTypeElementIndex); diff --git a/src/cs/vim/Vim.Format/RoomService.cs b/src/cs/vim/Vim.Format/RoomService.cs new file mode 100644 index 00000000..1ea67d13 --- /dev/null +++ b/src/cs/vim/Vim.Format/RoomService.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Vim.Format.Geometry; +using Vim.Format.ObjectModel; +using Vim.LinqArray; +using Vim.Math3d; + +namespace Vim.Format +{ + public class ElementInRoom + { + public int ElementIndex { get; } + public int RoomIndex { get; } + + public ElementInRoom(int elementIndex, int roomIndex) + { + ElementIndex = elementIndex; + RoomIndex = roomIndex; + } + } + + public static class RoomService + { + public delegate bool GeometricElementInfoFilter(ElementInfo elementInfo); + + public static ElementInRoom[] ComputeElementsInRoom(VimScene vim, GeometricElementInfoFilter geometricElementInfoFilter) + { + var dm = vim.DocumentModel; + + var roomElementIndices = new HashSet(dm.RoomElementIndex.ToEnumerable()); + if (roomElementIndices.Count == 0) + return Array.Empty(); // no rooms found. + + // Collect the bounding boxes of geometric elements + var geometricNodesGroupedByElementIndex = vim.VimNodes + .Where(n => n.HasMesh) + .GroupBy(n => n.ElementIndex) + .ToArray(); + + var roomGeometryCollection = geometricNodesGroupedByElementIndex + .AsParallel() + .Where(g => + { + var elementIndex = g.Key; + return roomElementIndices.Contains(elementIndex); + }) + .Select(g => + { + // ASSUMPTION: Rooms are typically represented by a single geometric node, so take the first item in the group. + var worldSpaceMesh = g.First().TransformedMesh(); + var roomElementIndex = g.Key; + + var roomIndexFound = dm.ElementIndexMaps.RoomIndexFromElementIndex.TryGetValue(roomElementIndex, out var roomIndex); + roomIndex = roomIndexFound ? roomIndex : -1; + + return new RoomGeometry(roomIndex, worldSpaceMesh.Vertices.ToArray(), worldSpaceMesh.Indices.ToArray()); + }) + .ToArray(); + + if (roomGeometryCollection.Length == 0) + return Array.Empty(); // no rooms with geometry found. + + var filteredElementGeometricNodes = geometricNodesGroupedByElementIndex + .AsParallel() + .Where(g => + { + // Filter the elements + var elementInfo = g.First(); + if (!geometricElementInfoFilter(elementInfo)) + return false; + + // Ignore room elements. + if (roomElementIndices.Contains(elementInfo.ElementIndex)) + return false; + + return true; + }).Select(g => + { + var elementIndex = g.Key; + + // Determine whether the element geometry's bounding box center is contained in one of the rooms. + var boundingBox = g.Select(n => n.TransformedBoundingBox()).Aggregate((acc, cur) => acc.Merge(cur)); + var boxCenter = boundingBox.Center; + + foreach (var roomGeometry in roomGeometryCollection) + { + // Broad-phase bounding box check. + var containmentType = roomGeometry.AABox.Contains(boundingBox); + if (containmentType == ContainmentType.Disjoint) + continue; + + // Refined point-in-mesh check + if (roomGeometry.ContainsPoint(boxCenter)) + return new ElementInRoom(elementIndex, roomGeometry.RoomIndex); // early return on the first room which contains the bottom center of the box. + } + + return null; + }).Where(eir => eir != null) + .ToArray(); + + return filteredElementGeometricNodes; + } + + private class RoomGeometry + { + private readonly Vector3[] _vertices; + private readonly int[] _indices; + + public int RoomIndex { get; } + public AABox AABox { get; } + + public RoomGeometry(int roomIndex, Vector3[] vertices, int[] indices) + { + RoomIndex = roomIndex; + _vertices = vertices; + _indices = indices; + AABox = AABox.Create(vertices); + } + + public bool ContainsPoint(Vector3 point) + { + var intersections = 0; + + var ray = new Ray(point, Vector3.UnitZ); + + for (var i = 0; i < _indices.Length; i += 3) + { + var v0 = _vertices[_indices[i]]; + var v1 = _vertices[_indices[i + 1]]; + var v2 = _vertices[_indices[i + 2]]; + + var triangle = new Triangle(v0, v1, v2); + + if (ray.Intersects(triangle) != null) + { + intersections++; + } + } + + return intersections % 2 == 1; // Inside if odd intersections + } + } + } +}