From 1a892a0315fe7582f01f9a06feb6ad902f61740c Mon Sep 17 00:00:00 2001 From: Martin Ashton Date: Mon, 14 Apr 2025 23:34:59 -0400 Subject: [PATCH 1/2] (WIP) Improving TransformService to correctly trim unused entities. --- .../CascadeElementRemapAttribute.cs | 11 + .../ColumnExtensions.Reflection.cs | 3 + .../DocumentBuilderExtensions.cs | 40 ---- src/cs/vim/Vim.Format.Core/Geometry/IScene.cs | 44 +--- .../Geometry/SceneExtensions.cs | 14 -- .../Vim.Format.Tests/TransformServiceTests.cs | 8 + .../vim/Vim.Format/ObjectModel/ObjectModel.cs | 7 + .../Vim.Format/SceneBuilder/VimSceneNode.cs | 8 +- src/cs/vim/Vim.Format/TransformService.cs | 213 +++++++++++++++++- 9 files changed, 238 insertions(+), 110 deletions(-) create mode 100644 src/cs/vim/Vim.Format.Core/CascadeElementRemapAttribute.cs diff --git a/src/cs/vim/Vim.Format.Core/CascadeElementRemapAttribute.cs b/src/cs/vim/Vim.Format.Core/CascadeElementRemapAttribute.cs new file mode 100644 index 00000000..3e058e45 --- /dev/null +++ b/src/cs/vim/Vim.Format.Core/CascadeElementRemapAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace Vim.Format +{ + [AttributeUsage(AttributeTargets.Class)] + public class CascadeElementRemapAttribute : Attribute + { + public CascadeElementRemapAttribute() + { } + } +} diff --git a/src/cs/vim/Vim.Format.Core/ColumnExtensions.Reflection.cs b/src/cs/vim/Vim.Format.Core/ColumnExtensions.Reflection.cs index d7cedece..a5dc9872 100644 --- a/src/cs/vim/Vim.Format.Core/ColumnExtensions.Reflection.cs +++ b/src/cs/vim/Vim.Format.Core/ColumnExtensions.Reflection.cs @@ -75,6 +75,9 @@ public static IEnumerable GetRelationFields(this Type t) public static string GetEntityTableName(this Type t) => (t.GetCustomAttribute(typeof(TableNameAttribute)) as TableNameAttribute)?.Name; + public static bool HasCascadeElementRemap(this Type t) + => (t.GetCustomAttribute(typeof(CascadeElementRemapAttribute)) as CascadeElementRemapAttribute) != null; + public static (string IndexColumnName, string LocalFieldName) GetIndexColumnInfo(this FieldInfo fieldInfo) { if (!fieldInfo.Name.StartsWith("_")) diff --git a/src/cs/vim/Vim.Format.Core/DocumentBuilderExtensions.cs b/src/cs/vim/Vim.Format.Core/DocumentBuilderExtensions.cs index 81e1c2d8..543dc95c 100644 --- a/src/cs/vim/Vim.Format.Core/DocumentBuilderExtensions.cs +++ b/src/cs/vim/Vim.Format.Core/DocumentBuilderExtensions.cs @@ -33,46 +33,6 @@ public static SubdividedMesh ToDocumentBuilderSubdividedMesh(this IMesh m) public static void AddMesh(this DocumentBuilder db, IMesh m) => db.AddMesh(m.ToDocumentBuilderSubdividedMesh()); - public static EntityTableBuilder CreateTableCopy(this DocumentBuilder db, EntityTable table, List nodeIndexRemapping = null) - { - var name = table.Name; - var tb = db.CreateTableBuilder(name); - - foreach (var col in table.IndexColumns.Values.ToEnumerable()) - { - tb.AddIndexColumn(col.Name, col.GetTypedData().RemapData(nodeIndexRemapping)); - } - - foreach (var col in table.DataColumns.Values.ToEnumerable()) - { - tb.AddDataColumn(col.Name, col.CopyDataColumn(nodeIndexRemapping)); - } - - foreach (var col in table.StringColumns.Values.ToEnumerable()) - { - var strings = col.GetTypedData().Select(i => table.Document.StringTable.ElementAtOrDefault(i, null)); - tb.AddStringColumn(col.Name, strings.ToArray().RemapData(nodeIndexRemapping)); - } - - return tb; - } - - public static DocumentBuilder CopyTablesFrom(this DocumentBuilder db, Document doc, List nodeIndexRemapping = null) - { - foreach (var table in doc.EntityTables.Values.ToEnumerable()) - { - var name = table.Name; - - // Don't copy tables that are computed automatically - if (VimConstants.ComputedTableNames.Contains(name)) - continue; - - db.CreateTableCopy(table, name == TableNames.Node ? nodeIndexRemapping : null); - } - - return db; - } - public static SerializableEntityTable ToSerializableEntityTable(this EntityTableBuilder tb, IReadOnlyDictionary stringLookup) { diff --git a/src/cs/vim/Vim.Format.Core/Geometry/IScene.cs b/src/cs/vim/Vim.Format.Core/Geometry/IScene.cs index eb2e5821..43379e4c 100644 --- a/src/cs/vim/Vim.Format.Core/Geometry/IScene.cs +++ b/src/cs/vim/Vim.Format.Core/Geometry/IScene.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using Vim.LinqArray; +using Vim.LinqArray; using Vim.Math3d; namespace Vim.Format.Geometry @@ -18,50 +17,9 @@ public interface IScene /// public interface ISceneNode { - int Id { get; } - IScene Scene { get; } Matrix4x4 Transform { get; } int MeshIndex { get; } IMesh GetMesh(); ISceneNode Parent { get; } - - // TODO: DEPRECATE: this needs to be removed, currently only used in Vim.Max.Bridge. - IArray Children { get; } - } - - public class SceneNodeComparer : EqualityComparer, IComparer - { - public static readonly SceneNodeComparer Instance = new SceneNodeComparer(); - - public int Compare(ISceneNode x, ISceneNode y) - => x.Id - y.Id; - public override bool Equals(ISceneNode x, ISceneNode y) - => x.Id == y.Id; - public override int GetHashCode(ISceneNode obj) - => obj.Id; - } - - public class NullNode : ISceneNode - { - public static NullNode Instance = new NullNode(); - public static List ListInstance = new List() { Instance }; - public int Id => -1; - public IScene Scene => null; - public Matrix4x4 Transform => Matrix4x4.Identity; - public int MeshIndex => 0; - public ISceneNode Parent => null; - public IArray Children => null; - public IMesh GetMesh() => null; - } - - public class IdNode : ISceneNode - { - public int Id { get; set; } - public IScene Scene => null; - public Matrix4x4 Transform => Matrix4x4.Identity; - public int MeshIndex => 0; - public ISceneNode Parent => null; - public IArray Children => null; - public IMesh GetMesh() => null; } } diff --git a/src/cs/vim/Vim.Format.Core/Geometry/SceneExtensions.cs b/src/cs/vim/Vim.Format.Core/Geometry/SceneExtensions.cs index 8f6e2f9a..f4aa1bf6 100644 --- a/src/cs/vim/Vim.Format.Core/Geometry/SceneExtensions.cs +++ b/src/cs/vim/Vim.Format.Core/Geometry/SceneExtensions.cs @@ -17,20 +17,6 @@ public static IEnumerable TransformedMeshes(this IScene scene) public static IMesh ToIMesh(this IScene scene) => scene.TransformedMeshes().Merge(); - public static bool HasLoop(this ISceneNode n) - { - if (n == null) return false; - var visited = new HashSet(); - for (; n != null; n = n.Parent) - { - if (visited.Contains(n)) - return true; - visited.Add(n); - } - - return false; - } - public static IMesh MergedGeometry(this IScene scene) => scene.Nodes.ToEnumerable().MergedGeometry(); diff --git a/src/cs/vim/Vim.Format.Tests/TransformServiceTests.cs b/src/cs/vim/Vim.Format.Tests/TransformServiceTests.cs index 7e1ff070..6ee3445b 100644 --- a/src/cs/vim/Vim.Format.Tests/TransformServiceTests.cs +++ b/src/cs/vim/Vim.Format.Tests/TransformServiceTests.cs @@ -1,6 +1,7 @@ using NUnit.Framework; using System.IO; using System.Linq; +using Vim.Format.SceneBuilder; using Vim.LinqArray; using Vim.Util.Tests; @@ -34,6 +35,13 @@ public static void TestTransformService() Assert.IsTrue(File.Exists(transformedVimFilePath)); var transformedVim = VimScene.LoadVim(transformedVimFilePath); + transformedVim.Validate(); + Assert.IsTrue(transformedVim.VimNodes.ToEnumerable().All(n => n.CategoryName == "Walls")); + + var dm = transformedVim.DocumentModel; + Assert.IsTrue(dm.FamilyInstanceList.All(fi => fi.Element.Category.Name == "Walls")); + Assert.IsTrue(dm.NodeList.All(n => n.Element.Category.Name == "Walls")); + Assert.IsTrue(dm.ParameterList.All(p => p.Element.Category.Name == "Walls")); } } diff --git a/src/cs/vim/Vim.Format/ObjectModel/ObjectModel.cs b/src/cs/vim/Vim.Format/ObjectModel/ObjectModel.cs index 9b78cd85..d4fd565a 100644 --- a/src/cs/vim/Vim.Format/ObjectModel/ObjectModel.cs +++ b/src/cs/vim/Vim.Format/ObjectModel/ObjectModel.cs @@ -450,6 +450,7 @@ public ParameterDescriptorStorageType GetParameterDescriptorStorageType() /// Represents a parameter associated to an Element. An Element can contain 0..* Parameters. /// [TableName(TableNames.Parameter)] + [CascadeElementRemap] public partial class Parameter : EntityWithElement { /// @@ -768,6 +769,7 @@ public partial class FamilyType : EntityWithElement /// may have a length of 12 feet, whereas another FamilyInstance may have a different length of 8 feet. /// [TableName(TableNames.FamilyInstance)] + [CascadeElementRemap] public partial class FamilyInstance : EntityWithElement { public bool FacingFlipped; @@ -922,6 +924,7 @@ public DAABox2D Outline /// An associative table binding an Element to a View. /// [TableName(TableNames.ElementInView)] + [CascadeElementRemap] public partial class ElementInView : EntityWithElement, IStorageKey { public Relation _View; @@ -1155,6 +1158,7 @@ public DVector2 NormalUvOffset /// An associative table binding a Material to an Element. /// [TableName(TableNames.MaterialInElement)] + [CascadeElementRemap] public partial class MaterialInElement : EntityWithElement, IStorageKey { public double Area; @@ -1224,6 +1228,7 @@ public partial class CompoundStructure : Entity /// This serves to bridge the gap between the Element entities and their corresponding instance geometry. /// [TableName(TableNames.Node)] + [CascadeElementRemap] [G3dAttributeReference("g3d:instance:transform:0:float32:16", G3dAttributeReferenceMultiplicity.OneToOne)] [G3dAttributeReference("g3d:instance:parent:0:int32:1", G3dAttributeReferenceMultiplicity.OneToOne)] [G3dAttributeReference("g3d:instance:mesh:0:int32:1", G3dAttributeReferenceMultiplicity.OneToOne)] @@ -1363,6 +1368,7 @@ public enum ElementInSystemRole /// An associative table binding an Element to a System. /// [TableName(TableNames.ElementInSystem)] + [CascadeElementRemap] public partial class ElementInSystem : EntityWithElement, IStorageKey { /// @@ -1411,6 +1417,7 @@ public partial class Warning : Entity /// An associative table binding an Element to a Warning. /// [TableName(TableNames.ElementInWarning)] + [CascadeElementRemap] public partial class ElementInWarning : EntityWithElement, IStorageKey { public Relation _Warning; diff --git a/src/cs/vim/Vim.Format/SceneBuilder/VimSceneNode.cs b/src/cs/vim/Vim.Format/SceneBuilder/VimSceneNode.cs index 5364b04c..6ec1d6f8 100644 --- a/src/cs/vim/Vim.Format/SceneBuilder/VimSceneNode.cs +++ b/src/cs/vim/Vim.Format/SceneBuilder/VimSceneNode.cs @@ -20,8 +20,6 @@ public VimSceneNode(VimScene scene, int nodeIndex, int geometryIndex, Matrix4x4 public VimScene _Scene { get; } - public IScene Scene => _Scene; - public int Id => NodeIndex; public Matrix4x4 Transform { get; } public InstanceFlags InstanceFlags @@ -40,16 +38,12 @@ public IMesh GetMesh() public bool HasMesh => MeshIndex != -1; - public Node NodeModel => _Scene.DocumentModel.GetNode(Id); - public Geometry GeometryModel => _Scene.DocumentModel.GetGeometry(MeshIndex); - // TODO: I think this should be "IEnumerable" in the interface public ISceneNode Parent => null; - public IArray Children => LinqArray.LinqArray.Empty(); public string DisciplineName => VimSceneHelpers.GetDisiplineFromCategory(CategoryName); VimSceneNode ITransformable3D.Transform(Matrix4x4 mat) - => new VimSceneNode(_Scene, Id, MeshIndex, mat * Transform); + => new VimSceneNode(_Scene, NodeIndex, MeshIndex, mat * Transform); } } diff --git a/src/cs/vim/Vim.Format/TransformService.cs b/src/cs/vim/Vim.Format/TransformService.cs index 780b9f29..ceebf29e 100644 --- a/src/cs/vim/Vim.Format/TransformService.cs +++ b/src/cs/vim/Vim.Format/TransformService.cs @@ -58,15 +58,19 @@ public DocumentBuilder Transform( var db = new DocumentBuilder(_generatorString, SchemaVersion.Current, _versionString); // Filter the nodes. + var filteredNodeElementIndices = new HashSet(); var filteredNodes = new List(); var vimNodes = vim.VimNodes; for (var i = 0; i < vimNodes.Count; ++i) { var n = vimNodes[i]; if (nodeFilter?.Invoke(n) ?? true) + { filteredNodes.Add(n); + filteredNodeElementIndices.Add(n.ElementIndex); + } } - + // Filter the meshes. var filteredMeshes = new List(); foreach (var n in filteredNodes) @@ -123,17 +127,214 @@ public DocumentBuilder Transform( var g = meshLookup.GetOrDefaultAllowNulls(node.GetMesh()); var meshIndex = meshIndices.GetOrDefaultAllowNulls(g, -1); var transform = nodeTransform?.Invoke(node) ?? node.Transform; - nodeIndexRemapping.Add(node.Id); + nodeIndexRemapping.Add(node.NodeIndex); db.AddInstance(transform, meshIndex); } + + + // Calculate the entity remapping + var entityRemaps = EntityRemap.GetEntityRemaps(vim, filteredNodeElementIndices); + // Copy the tables - db.CopyTablesFrom(vim.Document, nodeIndexRemapping); + StoreTransformedEntityTables(vim.Document, db, entityRemaps); // Construct the document return db; } + public class EntityRemap + { + public string EntityTableName { get; } + public List RemappedIndices { get; } + public Dictionary OldToNewIndexMap { get; } + + /// + /// Constructor + /// + private EntityRemap(EntityTable entityTable, HashSet filteredIndices) + { + EntityTableName = entityTable.Name; + RemappedIndices = new List(); + OldToNewIndexMap = new Dictionary(); + + for (var entityIndex = 0; entityIndex < entityTable.NumRows; ++entityIndex) + { + if (!filteredIndices.Contains(entityIndex)) + continue; + + var newEntityIndex = RemappedIndices.Count; + RemappedIndices.Add(entityIndex); + OldToNewIndexMap.Add(entityIndex, newEntityIndex); + } + } + + public static Dictionary GetEntityRemaps(VimScene vim, HashSet filteredNodeElementIndices) + { + // Start by presuming that all element indices will be preserved + var dm = vim.DocumentModel; + + var filteredElementIndices = new HashSet(); + for (var elementIndex = 0; elementIndex < dm.NumElement; ++elementIndex) + { + filteredElementIndices.Add(elementIndex); + } + + // Next, discard any family instance elements which are not preserved. + for (var i = 0; i < dm.NumFamilyInstance; ++i) + { + var familyInstanceElementIndex = dm.FamilyInstanceElementIndex[i]; + if (!filteredNodeElementIndices.Contains(familyInstanceElementIndex)) + { + // Discard elements associated to family instances which will not appear in the remapping. + filteredElementIndices.Remove(familyInstanceElementIndex); + } + } + + var entityRemaps = new Dictionary(); + + // Add the element table to the remapped set. + if (TryGet(vim, TableNames.Element, filteredElementIndices, out var elementRemap)) + { + entityRemaps.Add(TableNames.Element, elementRemap); + } + + var cascadeElementRemapTableNames = ObjectModelReflection.GetEntityTypes() + .Where(t => t.HasCascadeElementRemap()) + .Select(t => t.GetEntityTableName()); + + // Node, FamilyInstance, Parameter, ElementInSystem, ... + foreach (var entityTableName in cascadeElementRemapTableNames) + { + // Add the cascading entity table to the remapped set. + if (TryGetCascadeElementRemap(vim, entityTableName, filteredElementIndices, out var remap)) + { + entityRemaps.Add(entityTableName, remap); + } + } + + return entityRemaps; + } + + private static bool TryGet(VimScene vim, string entityTableName, HashSet filteredIndices, out EntityRemap remap) + { + remap = null; + + var entityTables = vim.Document.EntityTables; + + if (!entityTables.Contains(entityTableName)) + return false; + + remap = new EntityRemap(entityTables[entityTableName], filteredIndices); + + return true; + } + + private static bool TryGetCascadeElementRemap(VimScene vim, string entityTableName, HashSet filteredElementIndices, out EntityRemap remap) + { + remap = null; + + var entityTables = vim.Document.EntityTables; + + if (!entityTables.Contains(entityTableName)) + return false; + + var entityTable = entityTables[entityTableName]; + var indexColumns = entityTable.IndexColumns; + if (indexColumns.Keys.Count == 0) + return false; + + var elementIndexColumnName = indexColumns.Keys + .FirstOrDefault(columnName => DocumentExtensions.GetRelatedTableNameFromColumnName(columnName) == TableNames.Element); + + if (string.IsNullOrWhiteSpace(elementIndexColumnName)) + return false; + + var filteredEntityIndices = new HashSet(); + var elementIndexColumn = indexColumns[elementIndexColumnName].Array; + for (var i = 0; i < elementIndexColumn.Length; ++i) + { + var elementIndex = elementIndexColumn[i]; + + if (filteredElementIndices.Contains(elementIndex)) + { + filteredEntityIndices.Add(i); + } + } + + remap = new EntityRemap(entityTable, filteredEntityIndices); + + return true; + } + } + + private static void StoreTransformedEntityTables( + Document sourceDocument, + DocumentBuilder db, + Dictionary entityRemaps = null) + { + // Iterate over all the source tables. + foreach (var sourceEntityTable in sourceDocument.EntityTables.Values.ToEnumerable()) + { + var entityTableName = sourceEntityTable.Name; + + // Don't copy tables that are computed automatically + if (VimConstants.ComputedTableNames.Contains(entityTableName)) + continue; + + var remappedIndices = entityRemaps != null && entityRemaps.TryGetValue(entityTableName, out var entityRemap) + ? entityRemap.RemappedIndices + : null; + + StoreTransformedTable(sourceEntityTable, db, remappedIndices, entityRemaps); + } + } + + public static EntityTableBuilder StoreTransformedTable( + EntityTable sourceTable, + DocumentBuilder db, + List remapping = null, + Dictionary entityRemaps = null) + { + var name = sourceTable.Name; + var tb = db.CreateTableBuilder(name); + + foreach (var col in sourceTable.IndexColumns.Values.ToEnumerable()) + { + var remappedIndexRelations = col.GetTypedData().RemapData(remapping); + + // Remap the referenced indices. + var relatedTableName = col.GetRelatedTableName(); + if (entityRemaps?.TryGetValue(relatedTableName, out var entityRemap) ?? false) + { + for (var i = 0; i < remappedIndexRelations.Length; ++i) + { + var oldIndex = remappedIndexRelations[i]; + var newIndex = entityRemap.OldToNewIndexMap.TryGetValue(oldIndex, out var ni) + ? ni + : EntityRelation.None; + + remappedIndexRelations[i] = newIndex; + } + } + + tb.AddIndexColumn(col.Name, remappedIndexRelations); + } + + foreach (var col in sourceTable.DataColumns.Values.ToEnumerable()) + { + tb.AddDataColumn(col.Name, col.CopyDataColumn(remapping)); + } + + foreach (var col in sourceTable.StringColumns.Values.ToEnumerable()) + { + var strings = col.GetTypedData().Select(i => sourceTable.Document.StringTable.ElementAtOrDefault(i, null)); + tb.AddStringColumn(col.Name, strings.ToArray().RemapData(remapping)); + } + + return tb; + } + /// /// Returns a new VIM document builder in which the original meshes have been deduplicated. /// @@ -149,10 +350,10 @@ public DocumentBuilder DeduplicateGeometry(VimScene vim) /// /// Returns a new VIM document builder in which the nodes have been filtered. /// - public DocumentBuilder Filter(VimScene vim, NodeFilter filter, bool deduplicateMeshes = false) + public DocumentBuilder Filter(VimScene vim, NodeFilter nodeFilter, bool deduplicateMeshes = false) => Transform( - vim, - filter, + vim, + nodeFilter, _ => true, n => n.Transform, m => m, From f7f596272991463fb31870acebcb02e2ad1909d8 Mon Sep 17 00:00:00 2001 From: Martin Ashton Date: Wed, 16 Apr 2025 11:10:22 -0400 Subject: [PATCH 2/2] Updated transform service and tests --- data/Dwelling.r2019.om_v5.5.0.vim | 3 + .../ColumnExtensions.Buffer.cs | 64 ++++- src/cs/vim/Vim.Format.Core/DocumentBuilder.cs | 6 +- .../Vim.Format.Tests/TransformServiceTests.cs | 172 +++++++++++- .../Merge/RemappedEntityTableBuilder.cs | 2 +- .../vim/Vim.Format/ObjectModel/ObjectModel.cs | 2 +- src/cs/vim/Vim.Format/TransformService.cs | 258 ++++++++++-------- 7 files changed, 371 insertions(+), 136 deletions(-) create mode 100644 data/Dwelling.r2019.om_v5.5.0.vim diff --git a/data/Dwelling.r2019.om_v5.5.0.vim b/data/Dwelling.r2019.om_v5.5.0.vim new file mode 100644 index 00000000..b043fa7c --- /dev/null +++ b/data/Dwelling.r2019.om_v5.5.0.vim @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbefdb5bd6fa39560a9bd90f7aaa3e8f41571f53b2fe403224720da091ce4c44 +size 173716608 diff --git a/src/cs/vim/Vim.Format.Core/ColumnExtensions.Buffer.cs b/src/cs/vim/Vim.Format.Core/ColumnExtensions.Buffer.cs index 3cce6b9f..1957bf1e 100644 --- a/src/cs/vim/Vim.Format.Core/ColumnExtensions.Buffer.cs +++ b/src/cs/vim/Vim.Format.Core/ColumnExtensions.Buffer.cs @@ -48,9 +48,6 @@ public static string ValidateCanConcatBuffers(this INamedBuffer thisBuffer, INam return thisPrefix; } - public static T[] RemapData(this T[] self, List remapping = null) - => remapping?.Select(x => self[x])?.ToArray() ?? self; - public static IBuffer ToBuffer(this T[] array) where T : unmanaged => new Buffer(array); @@ -97,29 +94,72 @@ public static IBuffer CreateDefaultDataColumnBuffer(int length, string typePrefi } } - public static IBuffer CopyDataColumn(this IBuffer dataColumn, string typePrefix, List remapping = null) + public static IBuffer RemapOrSelfDataColumn(this IBuffer dataColumn, string typePrefix, List remapping = null) { switch (typePrefix) { case (VimConstants.IntColumnNameTypePrefix): - return (dataColumn.Data as int[]).RemapData(remapping).ToBuffer(); + return (dataColumn.Data as int[]).RemapOrSelf(remapping).ToBuffer(); case (VimConstants.LongColumnNameTypePrefix): - return (dataColumn.Data as long[]).RemapData(remapping).ToBuffer(); + return (dataColumn.Data as long[]).RemapOrSelf(remapping).ToBuffer(); case (VimConstants.DoubleColumnNameTypePrefix): - return (dataColumn.Data as double[]).RemapData(remapping).ToBuffer(); + return (dataColumn.Data as double[]).RemapOrSelf(remapping).ToBuffer(); case (VimConstants.FloatColumnNameTypePrefix): - return (dataColumn.Data as float[]).RemapData(remapping).ToBuffer(); + return (dataColumn.Data as float[]).RemapOrSelf(remapping).ToBuffer(); case (VimConstants.ByteColumnNameTypePrefix): - return (dataColumn.Data as byte[]).RemapData(remapping).ToBuffer(); + return (dataColumn.Data as byte[]).RemapOrSelf(remapping).ToBuffer(); default: - throw new Exception($"{nameof(CopyDataColumn)} - {UnknownNamedBufferPrefix}"); + throw new Exception($"{nameof(RemapOrSelfDataColumn)} - {UnknownNamedBufferPrefix}"); } } - public static INamedBuffer CopyDataColumn(this INamedBuffer dataColumn, List remapping = null) + public static INamedBuffer RemapOrSelfDataColumn(this INamedBuffer dataColumn, List remapping = null) { var typePrefix = dataColumn.GetTypePrefix(); - return new NamedBuffer(dataColumn.CopyDataColumn(typePrefix, remapping), dataColumn.Name); + return new NamedBuffer(dataColumn.RemapOrSelfDataColumn(typePrefix, remapping), dataColumn.Name); + } + + public static T[] RemapOrSelf(this T[] source, List remapping = null) + { + if (remapping == null) + return source; + + var dst = new T[remapping.Count]; + + for (var i = 0; i < dst.Length; ++i) + { + var remappedIndex = remapping[i]; + dst[i] = source[remappedIndex]; + } + + return dst; + } + + public static T[] Copy(this T[] source, List remapping = null) + { + T[] result; + + if (remapping == null) + { + // Copy from the source. + result = new T[source.Length]; + + for (var i = 0; i < result.Length; ++i) + result[i] = source[i]; + } + else + { + // Copy from the remapping. + result = new T[remapping.Count]; + + for (var i = 0; i < result.Length; ++i) + { + var remappedIndex = remapping[i]; + result[i] = source[remappedIndex]; + } + } + + return result; } public static IBuffer Concat(this IBuffer thisBuffer, IBuffer otherBuffer) where T : unmanaged diff --git a/src/cs/vim/Vim.Format.Core/DocumentBuilder.cs b/src/cs/vim/Vim.Format.Core/DocumentBuilder.cs index a1d54198..a906e95e 100644 --- a/src/cs/vim/Vim.Format.Core/DocumentBuilder.cs +++ b/src/cs/vim/Vim.Format.Core/DocumentBuilder.cs @@ -5,6 +5,7 @@ using Vim.Math3d; using Vim.BFast; using System.IO; +using Vim.G3d; using Vim.Util; namespace Vim.Format @@ -83,14 +84,15 @@ public DocumentBuilder AddShapes(IEnumerable sb) public DocumentBuilder AddAsset(INamedBuffer b) => AddAsset(b.Name, b.ToBytes()); - public DocumentBuilder AddInstance(Matrix4x4 transform, int meshIndex, int parentIndex = -1) + public DocumentBuilder AddInstance(Matrix4x4 transform, int meshIndex, InstanceFlags flags, int parentIndex = -1) { Instances.Add( new Instance() { Transform = transform, MeshIndex = meshIndex, - ParentIndex = parentIndex + ParentIndex = parentIndex, + InstanceFlags = flags, } ); diff --git a/src/cs/vim/Vim.Format.Tests/TransformServiceTests.cs b/src/cs/vim/Vim.Format.Tests/TransformServiceTests.cs index 6ee3445b..9a8e4b29 100644 --- a/src/cs/vim/Vim.Format.Tests/TransformServiceTests.cs +++ b/src/cs/vim/Vim.Format.Tests/TransformServiceTests.cs @@ -1,8 +1,14 @@ using NUnit.Framework; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; +using Vim.Format.Geometry; +using Vim.Format.Merge; +using Vim.Format.ObjectModel; using Vim.Format.SceneBuilder; using Vim.LinqArray; +using Vim.Math3d; using Vim.Util.Tests; namespace Vim.Format.Tests; @@ -10,26 +16,28 @@ namespace Vim.Format.Tests; [TestFixture] public static class TransformServiceTests { - [Test] - public static void TestTransformService() + [TestCase("Walls")] + [TestCase("Doors")] + [TestCase("BOGUS_CATEGORY_THIS_REMOVES_ALL_NODES")] + public static void TestTransformService(string categoryName) { - var ctx = new CallerTestContext(); + var ctx = new CallerTestContext(subDirComponents: categoryName); var dir = ctx.PrepareDirectory(); var vimFilePath = Path.Combine(VimFormatRepoPaths.DataDir, "RoomTest.vim"); var vim = VimScene.LoadVim(vimFilePath); - // Just keep the walls. + // Keep the nodes of the given category and rotate them upside down. var transformService = new TransformService(nameof(TestTransformService), "0.0.0"); var db = transformService.Transform( vim, - n => n.CategoryName == "Walls", - null, + n => n.CategoryName == categoryName, null, + n => Matrix4x4.CreateFromAxisAngle(Vector3.UnitX, (float)Math.PI) * n.Transform, null, false); - var transformedVimFilePath = Path.Combine(dir, "rooms_transformed.vim"); + var transformedVimFilePath = Path.Combine(dir, $"transformed_{categoryName}.vim"); db.Write(transformedVimFilePath); Assert.IsTrue(File.Exists(transformedVimFilePath)); @@ -37,11 +45,153 @@ public static void TestTransformService() var transformedVim = VimScene.LoadVim(transformedVimFilePath); transformedVim.Validate(); - Assert.IsTrue(transformedVim.VimNodes.ToEnumerable().All(n => n.CategoryName == "Walls")); + Assert.IsTrue(transformedVim.VimNodes.ToEnumerable().All(n => n.CategoryName == categoryName)); var dm = transformedVim.DocumentModel; - Assert.IsTrue(dm.FamilyInstanceList.All(fi => fi.Element.Category.Name == "Walls")); - Assert.IsTrue(dm.NodeList.All(n => n.Element.Category.Name == "Walls")); - Assert.IsTrue(dm.ParameterList.All(p => p.Element.Category.Name == "Walls")); + Assert.IsTrue(dm.NodeList.All(n => n.Element.Category.Name == categoryName)); + + foreach (var fi in dm.FamilyInstanceList.ToArray()) + { + var elementInfo = dm.GetElementInfo(fi.Element.Index); + if (elementInfo.IsSystem) // TECH DEBT - skipping systems for now. + continue; + + Assert.AreEqual(categoryName, elementInfo.CategoryName, $"FamilyInstance Element category is not {categoryName}"); + } + } + + [Test] + public static void TestSplitMerge() + { + var ctx = new CallerTestContext(); + var dir = ctx.PrepareDirectory(); + + var vimFilePath = Path.Combine(VimFormatRepoPaths.DataDir, "RoomTest.vim"); + var vim = VimScene.LoadVim(vimFilePath); + var transformService = new TransformService(nameof(TestTransformService), "0.0.0"); + + var box = vim.BoundingBox(); + + bool IsLeft(VimSceneNode n) + => n.TransformedMesh().Center().X < box.Center.X; + + // Get the left part of the VIM file + var leftVimFilePath = Path.Combine(dir, "left.vim"); + { + var db = transformService.Filter(vim, n => n.GetMesh() != null && IsLeft(n)); + db.Write(leftVimFilePath); + Assert.IsTrue(File.Exists(leftVimFilePath)); + } + var leftVim = VimScene.LoadVim(leftVimFilePath); + leftVim.Validate(); + + // Get the right part of the VIM file. + var rightVimFilePath = Path.Combine(dir, "right.vim"); + { + var db = transformService.Filter(vim, n => n.GetMesh() != null && !IsLeft(n)); + db.Write(rightVimFilePath); + Assert.IsTrue(File.Exists(rightVimFilePath)); + } + var rightVim = VimScene.LoadVim(rightVimFilePath); + rightVim.Validate(); + + + bool HasGeometry(ISceneNode n) => n.GetMesh() != null; + Assert.AreEqual(vim.Nodes.Where(HasGeometry).Count(), leftVim.Nodes.Where(HasGeometry).Count() + rightVim.Nodes.Where(HasGeometry).Count()); + + Assert.AreEqual(vim.Document.EntityTables.Keys.Count, leftVim.Document.EntityTables.Keys.Count); + Assert.AreEqual(vim.Document.EntityTables.Keys.Count, rightVim.Document.EntityTables.Keys.Count); + + var mergedVimFilePath = Path.Combine(dir, "merged.vim"); + var documentBuilder = MergeService.MergeVimScenes(new MergeConfigVimScenes(new[] { leftVim, rightVim })); + documentBuilder.Write(mergedVimFilePath); + Assert.IsTrue(File.Exists(mergedVimFilePath)); + var mergedVim = VimScene.LoadVim(mergedVimFilePath); + mergedVim.Validate(); + + Assert.AreEqual(vim.Nodes.Where(HasGeometry).Count(), mergedVim.Nodes.Where(HasGeometry).Count()); + + Assert.AreEqual(vim.Document.EntityTables.Keys.Count, vim.Document.EntityTables.Keys.Count); + + var sourceStringSet = new HashSet(vim.Document.StringTable.ToEnumerable()); + var mergedStringSet = new HashSet(mergedVim.Document.StringTable.ToEnumerable()); + mergedStringSet.ExceptWith(sourceStringSet); + Assert.AreEqual(0, mergedStringSet.Count); + + Assert.AreEqual(vim.Document.Assets.Keys.Count, mergedVim.Document.Assets.Keys.Count); + } + + [Test] + public static void TestFilter() + { + var ctx = new CallerTestContext(); + var dir = ctx.PrepareDirectory(); + + var vimFilePath = Path.Combine(VimFormatRepoPaths.DataDir, "RoomTest.vim"); + var vim = VimScene.LoadVim(vimFilePath); + + // Just keep the even-numbered nodes. + var transformService = new TransformService(nameof(TestTransformService), "0.0.0"); + var counter = 0; + var db = transformService.Filter(vim, n => n.GetMesh() != null && (counter++ % 2 == 0)); + + var filteredVimFilePath = Path.Combine(dir, "evens.vim"); + db.Write(filteredVimFilePath); + + Assert.IsTrue(File.Exists(filteredVimFilePath)); + + var filteredVim = VimScene.LoadVim(filteredVimFilePath); + filteredVim.Validate(); + } + + [Test] + public static void TestMergeDedupAndFilter() + { + var ctx = new CallerTestContext(); + var dir = ctx.PrepareDirectory(); + + var vimFilePath = Path.Combine(VimFormatRepoPaths.DataDir, "Dwelling.r2019.om_v5.5.0.vim"); + + // Setup: Merge two identical VIM files as a grid. + var vim1 = VimScene.LoadVim(vimFilePath); + var vim2 = VimScene.LoadVim(vimFilePath); + + var generatorString = nameof(TestMergeDedupAndFilter); + var versionString = "0.0.0"; + + var mergedDb = MergeService.MergeVimScenes( + new MergeConfigVimScenes(new[] { vim1, vim2 }), + new MergeConfigOptions + { + MergeAsGrid = true, + GridPadding = 10f, + GeneratorString = generatorString, + VersionString = versionString + }); + + var mergedVimFilePath = Path.Combine(dir, "merged.vim"); + mergedDb.Write(mergedVimFilePath); + var mergedVim = VimScene.LoadVim(mergedVimFilePath); + mergedVim.Validate(); + + // Deduplicate identical meshes using the transform service. + + var transformService = new TransformService(generatorString, versionString); + + var dedupDb = transformService.DeduplicateGeometry(mergedVim); + var dedupVimFilePath = Path.Combine(dir, "dedup.vim"); + dedupDb.Write(dedupVimFilePath); + var dedupVim = VimScene.LoadVim(dedupVimFilePath); + dedupVim.Validate(); + + Assert.Less(dedupVim.Meshes.Count, mergedVim.Meshes.Count); + + // Bonus: filter the deduplicated VIM and only keep the windows + + var filteredDb = transformService.Filter(dedupVim, n => n.CategoryName == "Casework"); + var filteredVimFilePath = Path.Combine(dir, "filtered.vim"); + filteredDb.Write(filteredVimFilePath); + var filteredVim = VimScene.LoadVim(filteredVimFilePath); + filteredVim.Validate(); } } diff --git a/src/cs/vim/Vim.Format/Merge/RemappedEntityTableBuilder.cs b/src/cs/vim/Vim.Format/Merge/RemappedEntityTableBuilder.cs index 231bb52a..32f5b194 100644 --- a/src/cs/vim/Vim.Format/Merge/RemappedEntityTableBuilder.cs +++ b/src/cs/vim/Vim.Format/Merge/RemappedEntityTableBuilder.cs @@ -97,7 +97,7 @@ private static RemappedEntityTableBuilder CreateRemapped(EntityTableBuilder e var colName = kv.Key; var col = kv.Value; var typePrefix = colName.GetTypePrefix(); - var newCol = col.CopyDataColumn(typePrefix, retainedIndices); + var newCol = col.RemapOrSelfDataColumn(typePrefix, retainedIndices); remapped.AddDataColumn(colName, newCol); } diff --git a/src/cs/vim/Vim.Format/ObjectModel/ObjectModel.cs b/src/cs/vim/Vim.Format/ObjectModel/ObjectModel.cs index d4fd565a..e3a0b645 100644 --- a/src/cs/vim/Vim.Format/ObjectModel/ObjectModel.cs +++ b/src/cs/vim/Vim.Format/ObjectModel/ObjectModel.cs @@ -561,6 +561,7 @@ public Vector3 Position } [TableName(TableNames.Group)] + [CascadeElementRemap] // Groups can be family instances public partial class Group : EntityWithElement { public string GroupType; @@ -1228,7 +1229,6 @@ public partial class CompoundStructure : Entity /// This serves to bridge the gap between the Element entities and their corresponding instance geometry. /// [TableName(TableNames.Node)] - [CascadeElementRemap] [G3dAttributeReference("g3d:instance:transform:0:float32:16", G3dAttributeReferenceMultiplicity.OneToOne)] [G3dAttributeReference("g3d:instance:parent:0:int32:1", G3dAttributeReferenceMultiplicity.OneToOne)] [G3dAttributeReference("g3d:instance:mesh:0:int32:1", G3dAttributeReferenceMultiplicity.OneToOne)] diff --git a/src/cs/vim/Vim.Format/TransformService.cs b/src/cs/vim/Vim.Format/TransformService.cs index ceebf29e..4acb4813 100644 --- a/src/cs/vim/Vim.Format/TransformService.cs +++ b/src/cs/vim/Vim.Format/TransformService.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Vim.Format.Geometry; using Vim.LinqArray; @@ -10,8 +11,6 @@ namespace Vim.Format { public class TransformService { - // TODO - TECH DEBT: refactor generatorString + versionString into a common service pattern and apply to MergeService as well. - /// /// A string representing the application which is emitting the new VIM document. /// @@ -33,6 +32,42 @@ public TransformService(string generatorString, string versionString) _versionString = versionString; } + /// + /// Returns a new VIM document builder in which the original meshes have been deduplicated. + /// + public DocumentBuilder DeduplicateGeometry(VimScene vim) + => Transform( + vim, + _ => true, + _ => true, + n => n.Transform, + m => m, + true); + + /// + /// Returns a new VIM document builder in which the nodes have been filtered. + /// + public DocumentBuilder Filter(VimScene vim, NodeFilter nodeFilter, bool deduplicateMeshes = false) + => Transform( + vim, + nodeFilter, + _ => true, + n => n.Transform, + m => m, + deduplicateMeshes); + + /// + /// Returns a new VIM document builder in which the instance transforms have been multiplied by the given matrix. + /// + public DocumentBuilder TransformInstances(VimScene vim, Matrix4x4 matrix, bool deduplicateMeshes = false) + => Transform( + vim, + _ => true, + _ => true, + n => n.Transform * matrix, + m => m, + deduplicateMeshes); + public delegate bool NodeFilter(VimSceneNode node); public delegate Matrix4x4 NodeTransform(VimSceneNode node); public delegate bool MeshFilter(IMesh mesh); @@ -57,30 +92,34 @@ public DocumentBuilder Transform( { var db = new DocumentBuilder(_generatorString, SchemaVersion.Current, _versionString); - // Filter the nodes. - var filteredNodeElementIndices = new HashSet(); - var filteredNodes = new List(); + // Filter the nodes we want to keep + var nodesToKeep = new List(); + var nodeElementIndicesToKeep = new HashSet(); + var oldNodeIndexToNewNodeIndex = new Dictionary(); var vimNodes = vim.VimNodes; for (var i = 0; i < vimNodes.Count; ++i) { - var n = vimNodes[i]; - if (nodeFilter?.Invoke(n) ?? true) + var node = vimNodes[i]; + if (nodeFilter?.Invoke(node) ?? true) { - filteredNodes.Add(n); - filteredNodeElementIndices.Add(n.ElementIndex); + var oldNodeIndex = i; + var newNodeIndex = nodesToKeep.Count; + nodesToKeep.Add(node); + oldNodeIndexToNewNodeIndex.Add(oldNodeIndex, newNodeIndex); + nodeElementIndicesToKeep.Add(node.ElementIndex); } } - - // Filter the meshes. - var filteredMeshes = new List(); - foreach (var n in filteredNodes) + + // Filter the meshes we want to keep + var meshesToKeep = new List(); + foreach (var node in nodesToKeep) { - var mesh = n.GetMesh(); + var mesh = node.GetMesh(); if (mesh == null || mesh.NumFaces == 0) continue; if (meshFilter?.Invoke(mesh) ?? true) - filteredMeshes.Add(mesh); + meshesToKeep.Add(mesh); } var meshLookup = new Dictionary(); @@ -90,7 +129,7 @@ public DocumentBuilder Transform( { // Group meshes according to a hash function const float tolerance = 1f / 12f / 8f; - var groupedMeshes = filteredMeshes.GroupMeshesByHash(tolerance); + var groupedMeshes = meshesToKeep.GroupMeshesByHash(tolerance); // Create the lookup from old mesh to new mesh meshLookup = groupedMeshes @@ -99,16 +138,16 @@ public DocumentBuilder Transform( } else { - // Create a mapping from mesh to mesh - foreach (var m in filteredMeshes) + // Create a dummy mapping from mesh to mesh + foreach (var m in meshesToKeep) meshLookup.AddIfNotPresent(m, m); } - // The indexed set of meshes, is only the values + // Collect the mesh indices in an indexed set. foreach (var m in meshLookup.Values) meshIndices.Add(m); - // Add assets + // Add the assets foreach (var asset in vim.Document.Assets.Values.ToEnumerable()) db.AddAsset(asset); @@ -116,30 +155,30 @@ public DocumentBuilder Transform( var subdividedMeshes = meshIndices.OrderedKeys.Select(m => (meshTransform?.Invoke(m) ?? m).ToDocumentBuilderSubdividedMesh()); db.AddMeshes(subdividedMeshes); - // Add the materials. TODO: this could be improved to remove duplicate materials, but this would require a remapping among the material entities as well. + // Add the materials. + // TECH DEBT: this could be improved to remove duplicate materials, but this would require a remapping among the material entities as well. var materials = vim.Document.Geometry.Materials.Select(m => m.ToDocumentBuilderMaterial()).ToEnumerable(); db.AddMaterials(materials); - // Add nodes - var nodeIndexRemapping = new List(); - foreach (var node in filteredNodes) + // Remove the associated FamilyInstances and remap the entities. + var nodeEntityRemap = new EntityRemap( + TableNames.Node, + nodesToKeep.Select(n => n.NodeIndex).ToList(), + oldNodeIndexToNewNodeIndex + ); + var entityRemaps = EntityRemap.GetEntityRemaps(vim, nodeEntityRemap, nodeElementIndicesToKeep); + StoreTransformedEntityTables(vim.Document, db, entityRemaps); + + // Add the nodes + foreach (var node in nodesToKeep) { var g = meshLookup.GetOrDefaultAllowNulls(node.GetMesh()); var meshIndex = meshIndices.GetOrDefaultAllowNulls(g, -1); var transform = nodeTransform?.Invoke(node) ?? node.Transform; - nodeIndexRemapping.Add(node.NodeIndex); - db.AddInstance(transform, meshIndex); + var flags = node.InstanceFlags; + db.AddInstance(transform, meshIndex, flags); } - - - // Calculate the entity remapping - var entityRemaps = EntityRemap.GetEntityRemaps(vim, filteredNodeElementIndices); - - // Copy the tables - StoreTransformedEntityTables(vim.Document, db, entityRemaps); - - // Construct the document return db; } @@ -149,10 +188,14 @@ public class EntityRemap public List RemappedIndices { get; } public Dictionary OldToNewIndexMap { get; } - /// - /// Constructor - /// - private EntityRemap(EntityTable entityTable, HashSet filteredIndices) + public EntityRemap(string entityTableName, List remappedIndices, Dictionary oldToNewIndexMap) + { + EntityTableName = entityTableName; + RemappedIndices = remappedIndices; + OldToNewIndexMap = oldToNewIndexMap; + } + + private EntityRemap(EntityTable entityTable, HashSet entityIndicesToKeep) { EntityTableName = entityTable.Name; RemappedIndices = new List(); @@ -160,7 +203,7 @@ private EntityRemap(EntityTable entityTable, HashSet filteredIndices) for (var entityIndex = 0; entityIndex < entityTable.NumRows; ++entityIndex) { - if (!filteredIndices.Contains(entityIndex)) + if (!entityIndicesToKeep.Contains(entityIndex)) continue; var newEntityIndex = RemappedIndices.Count; @@ -169,32 +212,50 @@ private EntityRemap(EntityTable entityTable, HashSet filteredIndices) } } - public static Dictionary GetEntityRemaps(VimScene vim, HashSet filteredNodeElementIndices) + public static Dictionary GetEntityRemaps( + VimScene vim, + EntityRemap nodeRemap, + HashSet nodeElementIndicesToKeep) { - // Start by presuming that all element indices will be preserved var dm = vim.DocumentModel; - - var filteredElementIndices = new HashSet(); + + // Start by presuming we will keep all the element indices. + var elementIndicesToKeep = new HashSet(); for (var elementIndex = 0; elementIndex < dm.NumElement; ++elementIndex) { - filteredElementIndices.Add(elementIndex); - } + var elementInfo = dm.GetElementInfo(elementIndex); - // Next, discard any family instance elements which are not preserved. - for (var i = 0; i < dm.NumFamilyInstance; ++i) - { - var familyInstanceElementIndex = dm.FamilyInstanceElementIndex[i]; - if (!filteredNodeElementIndices.Contains(familyInstanceElementIndex)) + var keep = true; + + if (elementInfo.IsSystem) + { + // TECH DEBT: for simplicity, we keep all elements which represent systems. + // Specifically, in the case of curtain walls in RoomTest.vim, an element + // can be -BOTH- a System and a FamilyInstance, which complicates things + // if we were to remove that element (we would also need to remove other + // ElementInSystem records referencing that removed system) + keep = true; + } + else if (elementInfo.IsFamilyInstance && !nodeElementIndicesToKeep.Contains(elementIndex)) { - // Discard elements associated to family instances which will not appear in the remapping. - filteredElementIndices.Remove(familyInstanceElementIndex); + // Here, the element represents a FamilyInstance, which we want to remove from the + // transformed collection of elements. + keep = false; + } + + if (keep) + { + elementIndicesToKeep.Add(elementIndex); } } - var entityRemaps = new Dictionary(); + var entityRemaps = new Dictionary() + { + { TableNames.Node, nodeRemap }, + }; // Add the element table to the remapped set. - if (TryGet(vim, TableNames.Element, filteredElementIndices, out var elementRemap)) + if (TryGet(vim, TableNames.Element, elementIndicesToKeep, out var elementRemap)) { entityRemaps.Add(TableNames.Element, elementRemap); } @@ -203,12 +264,12 @@ public static Dictionary GetEntityRemaps(VimScene vim, Hash .Where(t => t.HasCascadeElementRemap()) .Select(t => t.GetEntityTableName()); - // Node, FamilyInstance, Parameter, ElementInSystem, ... + // Compute the remapping for the following entity tables: Node, FamilyInstance, Parameter, ElementInSystem, ... foreach (var entityTableName in cascadeElementRemapTableNames) { - // Add the cascading entity table to the remapped set. - if (TryGetCascadeElementRemap(vim, entityTableName, filteredElementIndices, out var remap)) + if (TryGetCascadeElementRemap(vim, entityTableName, elementIndicesToKeep, out var remap)) { + // Add the cascading entity table to the remapped set. entityRemaps.Add(entityTableName, remap); } } @@ -216,7 +277,11 @@ public static Dictionary GetEntityRemaps(VimScene vim, Hash return entityRemaps; } - private static bool TryGet(VimScene vim, string entityTableName, HashSet filteredIndices, out EntityRemap remap) + private static bool TryGet( + VimScene vim, + string entityTableName, + HashSet filteredIndices, + out EntityRemap remap) { remap = null; @@ -230,7 +295,11 @@ private static bool TryGet(VimScene vim, string entityTableName, HashSet fi return true; } - private static bool TryGetCascadeElementRemap(VimScene vim, string entityTableName, HashSet filteredElementIndices, out EntityRemap remap) + private static bool TryGetCascadeElementRemap( + VimScene vim, + string entityTableName, + HashSet elementIndicesToKeep, + out EntityRemap remap) { remap = null; @@ -245,24 +314,24 @@ private static bool TryGetCascadeElementRemap(VimScene vim, string entityTableNa return false; var elementIndexColumnName = indexColumns.Keys - .FirstOrDefault(columnName => DocumentExtensions.GetRelatedTableNameFromColumnName(columnName) == TableNames.Element); + .FirstOrDefault(columnName => columnName == $"index:{TableNames.Element}:Element"); if (string.IsNullOrWhiteSpace(elementIndexColumnName)) return false; - var filteredEntityIndices = new HashSet(); - var elementIndexColumn = indexColumns[elementIndexColumnName].Array; - for (var i = 0; i < elementIndexColumn.Length; ++i) + var entityIndicesToKeep = new HashSet(); + var elementIndices = indexColumns[elementIndexColumnName].Array; + for (var i = 0; i < elementIndices.Length; ++i) { - var elementIndex = elementIndexColumn[i]; + var elementIndex = elementIndices[i]; - if (filteredElementIndices.Contains(elementIndex)) + if (elementIndicesToKeep.Contains(elementIndex)) { - filteredEntityIndices.Add(i); + entityIndicesToKeep.Add(i); } } - remap = new EntityRemap(entityTable, filteredEntityIndices); + remap = new EntityRemap(entityTable, entityIndicesToKeep); return true; } @@ -296,12 +365,12 @@ public static EntityTableBuilder StoreTransformedTable( List remapping = null, Dictionary entityRemaps = null) { - var name = sourceTable.Name; - var tb = db.CreateTableBuilder(name); + var sourceTableName = sourceTable.Name; + var tb = db.CreateTableBuilder(sourceTableName); foreach (var col in sourceTable.IndexColumns.Values.ToEnumerable()) { - var remappedIndexRelations = col.GetTypedData().RemapData(remapping); + var remappedIndexRelations = col.GetTypedData().Copy(remapping); // Remap the referenced indices. var relatedTableName = col.GetRelatedTableName(); @@ -314,6 +383,13 @@ public static EntityTableBuilder StoreTransformedTable( ? ni : EntityRelation.None; + if (newIndex == EntityRelation.None && + col.Name == $"index:{TableNames.Element}:Element" && + sourceTableName != TableNames.Node) // NOTE: special exception for the Node table which must always be 1:1 aligned with the g3d instances (and may not always have a backing element) + { + Debug.Fail($"Remapped index is {EntityRelation.None} in {sourceTableName} > {col.Name}"); + } + remappedIndexRelations[i] = newIndex; } } @@ -323,52 +399,16 @@ public static EntityTableBuilder StoreTransformedTable( foreach (var col in sourceTable.DataColumns.Values.ToEnumerable()) { - tb.AddDataColumn(col.Name, col.CopyDataColumn(remapping)); + tb.AddDataColumn(col.Name, col.RemapOrSelfDataColumn(remapping)); } foreach (var col in sourceTable.StringColumns.Values.ToEnumerable()) { var strings = col.GetTypedData().Select(i => sourceTable.Document.StringTable.ElementAtOrDefault(i, null)); - tb.AddStringColumn(col.Name, strings.ToArray().RemapData(remapping)); + tb.AddStringColumn(col.Name, strings.ToArray().RemapOrSelf(remapping)); } return tb; } - - /// - /// Returns a new VIM document builder in which the original meshes have been deduplicated. - /// - public DocumentBuilder DeduplicateGeometry(VimScene vim) - => Transform( - vim, - _ => true, - _ => true, - n => n.Transform, - m => m, - true); - - /// - /// Returns a new VIM document builder in which the nodes have been filtered. - /// - public DocumentBuilder Filter(VimScene vim, NodeFilter nodeFilter, bool deduplicateMeshes = false) - => Transform( - vim, - nodeFilter, - _ => true, - n => n.Transform, - m => m, - deduplicateMeshes); - - /// - /// Returns a new VIM document builder in which the instance transforms have been multiplied by the given matrix. - /// - public DocumentBuilder TransformInstances(VimScene vim, Matrix4x4 matrix, bool deduplicateMeshes = false) - => Transform( - vim, - _ => true, - _ => true, - n => n.Transform * matrix, - m => m, - deduplicateMeshes); } }