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
+ }
+ }
+ }
+}