From ed0a84261afc0c05fbf6fc97de6fd93a9f5ec8e1 Mon Sep 17 00:00:00 2001 From: Richard Olsen Date: Tue, 17 Sep 2024 09:54:33 +0200 Subject: [PATCH 1/3] Extend the scaffold optimizer framework to enable use of primitives as results from optimizations. Also, fix handling of instance IDs for split meshes. --- .../BatchUtils/ScaffoldOptimizerTests.cs | 11 ++- .../ScaffoldPartOptimizerTest.cs | 7 +- .../ScaffoldPartOptimizerTestPartA.cs | 18 ++++- .../ScaffoldPartOptimizerTestPartB.cs | 18 ++++- .../BatchUtils/ScaffoldOptimizer.cs | 79 ++++++++++++------- .../IScaffoldOptimizerResult.cs | 8 ++ .../IScaffoldPartOptimizer.cs | 7 +- .../ScaffoldOptimizerResult.cs | 52 ++++++++++++ 8 files changed, 162 insertions(+), 38 deletions(-) create mode 100644 CadRevealFbxProvider/BatchUtils/ScaffoldPartOptimizers/IScaffoldOptimizerResult.cs create mode 100644 CadRevealFbxProvider/BatchUtils/ScaffoldPartOptimizers/ScaffoldOptimizerResult.cs diff --git a/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldOptimizerTests.cs b/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldOptimizerTests.cs index 70298b27..87d5d0fc 100644 --- a/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldOptimizerTests.cs +++ b/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldOptimizerTests.cs @@ -155,6 +155,9 @@ APrimitive[] primitives [Test] public void CheckScaffoldOptimizerActivation_GivenTestPartOptimizers_VerifyingTheReturnedNode() { + ulong currentInstanceId = 100; + var onRequestNewInstanceId = () => currentInstanceId++; + // Set up the input CadRevealNode nodeA = CreateCadRevealNode("TestNode, Test A test").node; CadRevealNode nodeB = CreateCadRevealNode("TestNode, Test B test").node; @@ -173,10 +176,10 @@ public void CheckScaffoldOptimizerActivation_GivenTestPartOptimizers_VerifyingTh optimizer.AddPartOptimizer(optimizerB); // Invoke the optimizer - optimizer.OptimizeNode(nodeA); - optimizer.OptimizeNode(nodeB); - optimizer.OptimizeNode(nodeC); - optimizer.OptimizeNode(nodeD); + optimizer.OptimizeNode(nodeA, onRequestNewInstanceId); + optimizer.OptimizeNode(nodeB, onRequestNewInstanceId); + optimizer.OptimizeNode(nodeC, onRequestNewInstanceId); + optimizer.OptimizeNode(nodeD, onRequestNewInstanceId); // Check the results CheckGeometries(nodeA.Geometries, optimizerA.GetVerticesTruth(), optimizerA.GetIndicesTruth(), boundingBoxes); diff --git a/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTest.cs b/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTest.cs index 9eba64b6..25d73336 100644 --- a/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTest.cs +++ b/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTest.cs @@ -1,13 +1,18 @@ namespace CadRevealFbxProvider.Tests.BatchUtils.ScaffoldPartOptimizers; using System.Numerics; +using CadRevealComposer.Primitives; using CadRevealComposer.Tessellation; using CadRevealFbxProvider.BatchUtils.ScaffoldPartOptimizers; public abstract class ScaffoldPartOptimizerTest : IScaffoldPartOptimizer { public abstract string Name { get; } - public abstract Mesh[] Optimize(Mesh mesh); + public abstract IScaffoldOptimizerResult[] Optimize( + APrimitive basePrimitive, + Mesh mesh, + Func requestChildPartInstanceId + ); public abstract string[] GetPartNameTriggerKeywords(); public abstract List GetVerticesTruth(); diff --git a/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTestPartA.cs b/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTestPartA.cs index 93db305a..841814a6 100644 --- a/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTestPartA.cs +++ b/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTestPartA.cs @@ -1,7 +1,9 @@ namespace CadRevealFbxProvider.Tests.BatchUtils.ScaffoldPartOptimizers; using System.Numerics; +using CadRevealComposer.Primitives; using CadRevealComposer.Tessellation; +using CadRevealFbxProvider.BatchUtils.ScaffoldPartOptimizers; public class ScaffoldPartOptimizerTestPartA : ScaffoldPartOptimizerTest { @@ -20,9 +22,21 @@ public override string Name get { return "Part A test optimizer"; } } - public override Mesh[] Optimize(Mesh mesh) + public override IScaffoldOptimizerResult[] Optimize( + APrimitive basePrimitive, + Mesh mesh, + Func requestChildPartInstanceId + ) { - return [new Mesh(GetVerticesTruth().ToArray(), GetIndicesTruth().ToArray(), mesh.Error)]; + return + [ + new ScaffoldOptimizerResult( + basePrimitive, + new Mesh(GetVerticesTruth().ToArray(), GetIndicesTruth().ToArray(), mesh.Error), + 0, + requestChildPartInstanceId + ) + ]; } public override string[] GetPartNameTriggerKeywords() diff --git a/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTestPartB.cs b/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTestPartB.cs index cd991ee2..2ea0a1f9 100644 --- a/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTestPartB.cs +++ b/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTestPartB.cs @@ -1,7 +1,9 @@ namespace CadRevealFbxProvider.Tests.BatchUtils.ScaffoldPartOptimizers; using System.Numerics; +using CadRevealComposer.Primitives; using CadRevealComposer.Tessellation; +using CadRevealFbxProvider.BatchUtils.ScaffoldPartOptimizers; public class ScaffoldPartOptimizerTestPartB : ScaffoldPartOptimizerTest { @@ -20,9 +22,21 @@ public override string Name get { return "Part A test optimizer"; } } - public override Mesh[] Optimize(Mesh mesh) + public override IScaffoldOptimizerResult[] Optimize( + APrimitive basePrimitive, + Mesh mesh, + Func requestChildPartInstanceId + ) { - return [new Mesh(GetVerticesTruth().ToArray(), GetIndicesTruth().ToArray(), mesh.Error)]; + return + [ + new ScaffoldOptimizerResult( + basePrimitive, + new Mesh(GetVerticesTruth().ToArray(), GetIndicesTruth().ToArray(), mesh.Error), + 0, + requestChildPartInstanceId + ) + ]; } public override string[] GetPartNameTriggerKeywords() diff --git a/CadRevealFbxProvider/BatchUtils/ScaffoldOptimizer.cs b/CadRevealFbxProvider/BatchUtils/ScaffoldOptimizer.cs index f7756617..2a143a62 100644 --- a/CadRevealFbxProvider/BatchUtils/ScaffoldOptimizer.cs +++ b/CadRevealFbxProvider/BatchUtils/ScaffoldOptimizer.cs @@ -13,21 +13,21 @@ public void AddPartOptimizer(IScaffoldPartOptimizer optimizer) _partOptimizers.Add(optimizer); } - public void OptimizeNode(CadRevealNode node) + public void OptimizeNode(CadRevealNode node, Func requestNewInstanceId) { - var name = node.Name; + var partName = node.Name; var newGeometries = new List(); foreach (APrimitive primitive in node.Geometries) { - APrimitive[]? newPrimitiveList = OptimizePrimitive(primitive, name); + APrimitive[]? newPrimitiveList = OptimizePrimitive(primitive, partName, requestNewInstanceId); newGeometries.AddRange(newPrimitiveList ?? [primitive]); } node.Geometries = newGeometries.ToArray(); } - private APrimitive[]? OptimizePrimitive(APrimitive primitive, string name) + private APrimitive[]? OptimizePrimitive(APrimitive primitive, string partName, Func requestNewInstanceId) { // Handle only primitives that have their own Mesh objects, then pull out the mesh and optimize. These are the ones that need to be optimized. var primitiveList = new List(); @@ -35,40 +35,61 @@ public void OptimizeNode(CadRevealNode node) { case InstancedMesh instancedMesh: { - Mesh[] meshes = OptimizeMesh(instancedMesh.TemplateMesh, name); - primitiveList.AddRange( - meshes.Select(mesh => new InstancedMesh( - instancedMesh.InstanceId, - mesh, - instancedMesh.InstanceMatrix, - instancedMesh.TreeIndex, - instancedMesh.Color, - instancedMesh.AxisAlignedBoundingBox - )) - ); + OptimizeMeshAndAddResult(instancedMesh.TemplateMesh); break; } case TriangleMesh triangleMesh: { - Mesh[] meshes = OptimizeMesh(triangleMesh.Mesh, name); - primitiveList.AddRange( - meshes.Select(mesh => new TriangleMesh( - mesh, - triangleMesh.TreeIndex, - triangleMesh.Color, - triangleMesh.AxisAlignedBoundingBox - )) - ); + OptimizeMeshAndAddResult(triangleMesh.Mesh); break; } } return primitiveList.Count > 0 ? primitiveList.ToArray() : null; + + void OptimizeMeshAndAddResult(Mesh mesh) + { + IScaffoldOptimizerResult[] results = OptimizeMesh(primitive, mesh, partName, requestNewInstanceId); + primitiveList.AddRange(results.Select(result => result.Get())); + } } - private Mesh[] OptimizeMesh(Mesh mesh, string name) + private ulong OnRequestChildMeshInstanceId( + ulong instanceIdParentMesh, + int indexChildMesh, + Func requestNewInstanceId + ) { - Mesh[] optimizedMesh = [mesh]; + if (_childMeshInstanceIdLookup.TryGetValue(instanceIdParentMesh, out Dictionary? retInstanceIdDict)) + { + if (retInstanceIdDict.TryGetValue(indexChildMesh, out ulong retInstanceId)) + { + return retInstanceId; + } + + _childMeshInstanceIdLookup[instanceIdParentMesh].Add(indexChildMesh, requestNewInstanceId()); + return _childMeshInstanceIdLookup[instanceIdParentMesh][indexChildMesh]; + } + + _childMeshInstanceIdLookup.Add(instanceIdParentMesh, new Dictionary()); + _childMeshInstanceIdLookup[instanceIdParentMesh].Add(indexChildMesh, requestNewInstanceId()); + return _childMeshInstanceIdLookup[instanceIdParentMesh][indexChildMesh]; + } + + private IScaffoldOptimizerResult[] OptimizeMesh( + APrimitive basePrimitive, + Mesh mesh, + string name, + Func requestNewInstanceId + ) + { + var onRequestChildMeshInstanceId = (ulong instanceIdParentMesh, int indexChildMesh) => + OnRequestChildMeshInstanceId(instanceIdParentMesh, indexChildMesh, requestNewInstanceId); + + IScaffoldOptimizerResult[] optimizedResult = + [ + new ScaffoldOptimizerResult(basePrimitive, mesh, 0, onRequestChildMeshInstanceId) + ]; var triggeredOptimizers = new List(); foreach (IScaffoldPartOptimizer partOptimizer in _partOptimizers) { @@ -85,7 +106,7 @@ private Mesh[] OptimizeMesh(Mesh mesh, string name) { if (triggeredOptimizers.Count == 0) { - optimizedMesh = partOptimizer.Optimize(mesh); + optimizedResult = partOptimizer.Optimize(basePrimitive, mesh, onRequestChildMeshInstanceId); } triggeredOptimizers.Add(partOptimizer); } @@ -107,9 +128,11 @@ private Mesh[] OptimizeMesh(Mesh mesh, string name) } } - return optimizedMesh; + return optimizedResult; } + private readonly Dictionary> _childMeshInstanceIdLookup = + new Dictionary>(); private readonly List _partOptimizers = [ // :TODO: Fill in the available part optimizers here diff --git a/CadRevealFbxProvider/BatchUtils/ScaffoldPartOptimizers/IScaffoldOptimizerResult.cs b/CadRevealFbxProvider/BatchUtils/ScaffoldPartOptimizers/IScaffoldOptimizerResult.cs new file mode 100644 index 00000000..b342975f --- /dev/null +++ b/CadRevealFbxProvider/BatchUtils/ScaffoldPartOptimizers/IScaffoldOptimizerResult.cs @@ -0,0 +1,8 @@ +namespace CadRevealFbxProvider.BatchUtils.ScaffoldPartOptimizers; + +using CadRevealComposer.Primitives; + +public interface IScaffoldOptimizerResult +{ + public APrimitive Get(); +} diff --git a/CadRevealFbxProvider/BatchUtils/ScaffoldPartOptimizers/IScaffoldPartOptimizer.cs b/CadRevealFbxProvider/BatchUtils/ScaffoldPartOptimizers/IScaffoldPartOptimizer.cs index 0bb381ff..7e56932a 100644 --- a/CadRevealFbxProvider/BatchUtils/ScaffoldPartOptimizers/IScaffoldPartOptimizer.cs +++ b/CadRevealFbxProvider/BatchUtils/ScaffoldPartOptimizers/IScaffoldPartOptimizer.cs @@ -1,10 +1,15 @@ namespace CadRevealFbxProvider.BatchUtils.ScaffoldPartOptimizers; +using CadRevealComposer.Primitives; using CadRevealComposer.Tessellation; public interface IScaffoldPartOptimizer { public string Name { get; } - public Mesh[] Optimize(Mesh mesh); + public IScaffoldOptimizerResult[] Optimize( + APrimitive basePrimitive, // Primitive that contains the mesh + Mesh mesh, // The mesh + Func requestChildPartInstanceId // Function will be called when the optimizer needs an instance ID parameters are (index of child mesh, instance ID of base primitive) + ); public string[] GetPartNameTriggerKeywords(); } diff --git a/CadRevealFbxProvider/BatchUtils/ScaffoldPartOptimizers/ScaffoldOptimizerResult.cs b/CadRevealFbxProvider/BatchUtils/ScaffoldPartOptimizers/ScaffoldOptimizerResult.cs new file mode 100644 index 00000000..6c8f258a --- /dev/null +++ b/CadRevealFbxProvider/BatchUtils/ScaffoldPartOptimizers/ScaffoldOptimizerResult.cs @@ -0,0 +1,52 @@ +namespace CadRevealFbxProvider.BatchUtils.ScaffoldPartOptimizers; + +using CadRevealComposer.Primitives; +using CadRevealComposer.Tessellation; + +public class ScaffoldOptimizerResult : IScaffoldOptimizerResult +{ + public ScaffoldOptimizerResult( + APrimitive basePrimitive, + Mesh optimizedMesh, + int indexChildMesh, + Func requestChildMeshInstanceId + ) + { + switch (basePrimitive) + { + case InstancedMesh instancedMesh: + ulong instanceId = requestChildMeshInstanceId(instancedMesh.InstanceId, indexChildMesh); + _optimizedPrimitive = new InstancedMesh( + instanceId, + optimizedMesh, + instancedMesh.InstanceMatrix, + instancedMesh.TreeIndex, + instancedMesh.Color, + optimizedMesh.CalculateAxisAlignedBoundingBox() + ); + return; + case TriangleMesh triangleMesh: + _optimizedPrimitive = new TriangleMesh( + optimizedMesh, + triangleMesh.TreeIndex, + triangleMesh.Color, + optimizedMesh.CalculateAxisAlignedBoundingBox() + ); + return; + } + + _optimizedPrimitive = basePrimitive; + } + + public ScaffoldOptimizerResult(APrimitive optimizedPrimitive) + { + _optimizedPrimitive = optimizedPrimitive; + } + + public APrimitive Get() + { + return _optimizedPrimitive; + } + + private readonly APrimitive _optimizedPrimitive; +} From 1662cb384849e878d6e81b0673b29d45916c9352 Mon Sep 17 00:00:00 2001 From: Richard Olsen Date: Tue, 17 Sep 2024 10:37:16 +0200 Subject: [PATCH 2/3] Fix formatting issue. --- .../ScaffoldPartOptimizers/IScaffoldPartOptimizer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CadRevealFbxProvider/BatchUtils/ScaffoldPartOptimizers/IScaffoldPartOptimizer.cs b/CadRevealFbxProvider/BatchUtils/ScaffoldPartOptimizers/IScaffoldPartOptimizer.cs index 7e56932a..25a8f182 100644 --- a/CadRevealFbxProvider/BatchUtils/ScaffoldPartOptimizers/IScaffoldPartOptimizer.cs +++ b/CadRevealFbxProvider/BatchUtils/ScaffoldPartOptimizers/IScaffoldPartOptimizer.cs @@ -7,9 +7,9 @@ public interface IScaffoldPartOptimizer { public string Name { get; } public IScaffoldOptimizerResult[] Optimize( - APrimitive basePrimitive, // Primitive that contains the mesh - Mesh mesh, // The mesh - Func requestChildPartInstanceId // Function will be called when the optimizer needs an instance ID parameters are (index of child mesh, instance ID of base primitive) + APrimitive basePrimitive, // Primitive that contains the mesh + Mesh mesh, // The mesh + Func requestChildPartInstanceId // Function will be called when the optimizer needs an instance ID parameters are (index of child mesh, instance ID of base primitive) ); public string[] GetPartNameTriggerKeywords(); } From 05becc7e96a127a5ea667793037b54c4e27554c3 Mon Sep 17 00:00:00 2001 From: Richard Olsen Date: Mon, 30 Sep 2024 15:49:15 +0200 Subject: [PATCH 3/3] Extend scaffold optimizer framework tests. --- .../BatchUtils/ScaffoldOptimizerTests.cs | 409 ++++++++++++++++-- .../ScaffoldPartOptimizerTest.cs | 4 +- .../ScaffoldPartOptimizerTestPartA.cs | 16 +- .../ScaffoldPartOptimizerTestPartB.cs | 16 +- .../ScaffoldPartOptimizerTestPartC.cs | 102 +++++ 5 files changed, 488 insertions(+), 59 deletions(-) create mode 100644 CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTestPartC.cs diff --git a/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldOptimizerTests.cs b/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldOptimizerTests.cs index 87d5d0fc..ee934154 100644 --- a/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldOptimizerTests.cs +++ b/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldOptimizerTests.cs @@ -11,6 +11,20 @@ namespace CadRevealFbxProvider.Tests.BatchUtils; public class ScaffoldOptimizerTests { + private enum ETestPurpose + { + TestGeometryAssignment = 0, + TestInstancing + } + + private enum EOptimizers + { + None = -1, + A = 0, + B = 1, + C = 2 + } + private static Mesh CreateMesh( float x1, float y1, @@ -33,9 +47,12 @@ uint i3 ); } - private static (CadRevealNode node, List nodeMeshes, List boundingBoxes) CreateCadRevealNode( - string partName - ) + private static ( + CadRevealNode node, + List nodeMeshes, + List boundingBoxes, + List<(int i1, int i2)> instancePairs + ) CreateCadRevealNode(string partName, ETestPurpose testPurpose) { var mesh1 = CreateMesh(5, 5, 5, 7, 7, 7, 3, 3, 3, 0, 1, 2); var mesh2 = CreateMesh(1, 2, 3, 6, 7, 8, 9, 10, 11, 0, 1, 2); @@ -44,22 +61,50 @@ string partName var bbox1 = new BoundingBox(new Vector3(0, 0, 0), new Vector3(1, 1, 1)); var bbox2 = new BoundingBox(new Vector3(1, 2, 3), new Vector3(4, 5, 6)); - var node = new CadRevealNode - { - TreeIndex = 0, - Name = partName, - Parent = null, - Geometries = - [ - new InstancedMesh(1, mesh1, Matrix4x4.Identity, 1, Color.Black, bbox1), - new InstancedMesh(2, mesh2, Matrix4x4.Identity, 2, Color.Black, bbox1), - new TriangleMesh(mesh3, 3, Color.Black, bbox1), - new TriangleMesh(mesh1, 4, Color.Black, bbox1), - new Circle(Matrix4x4.Identity, new Vector3(1.0f, 8.0f, 2.0f), 5, Color.Black, bbox2) - ] - }; - - return (node, [mesh1, mesh2, mesh3, mesh1, null], [bbox1, bbox1, bbox1, bbox1, bbox2]); + switch (testPurpose) + { + case ETestPurpose.TestGeometryAssignment: + var node1 = new CadRevealNode + { + TreeIndex = 0, + Name = partName, + Parent = null, + Geometries = + [ + new InstancedMesh(1, mesh1, Matrix4x4.Identity, 1, Color.Black, bbox1), + new InstancedMesh(2, mesh2, Matrix4x4.Identity, 2, Color.Black, bbox1), + new TriangleMesh(mesh3, 3, Color.Black, bbox1), + new TriangleMesh(mesh1, 4, Color.Black, bbox1), + new Circle(Matrix4x4.Identity, new Vector3(1.0f, 8.0f, 2.0f), 5, Color.Black, bbox2) + ] + }; + return (node1, [mesh1, mesh2, mesh3, mesh1, null], [bbox1, bbox1, bbox1, bbox1, bbox2], []); + case ETestPurpose.TestInstancing: + var node2 = new CadRevealNode + { + TreeIndex = 0, + Name = partName, + Parent = null, + Geometries = + [ + new InstancedMesh(1, mesh1, Matrix4x4.Identity, 1, Color.Black, bbox1), + new InstancedMesh(1, mesh1, Matrix4x4.Identity, 2, Color.Black, bbox1), + new InstancedMesh(2, mesh2, Matrix4x4.Identity, 1, Color.Black, bbox1), + new InstancedMesh(2, mesh2, Matrix4x4.Identity, 2, Color.Black, bbox1), + new InstancedMesh(2, mesh2, Matrix4x4.Identity, 1, Color.Black, bbox1), + new InstancedMesh(3, mesh3, Matrix4x4.Identity, 2, Color.Black, bbox1), + new Circle(Matrix4x4.Identity, new Vector3(1.0f, 8.0f, 2.0f), 5, Color.Black, bbox2) + ] + }; + return ( + node2, + [mesh1, mesh1, mesh2, mesh2, mesh2, mesh3, null], + [bbox1, bbox1, bbox1, bbox1, bbox1, bbox1, bbox2], + [(0, 1), (2, 3), (2, 4)] + ); + default: + throw new ArgumentOutOfRangeException(nameof(testPurpose), testPurpose, null); + } } private static void CheckMeshList( @@ -103,19 +148,24 @@ private static void CheckPrimitive(APrimitive primitive, BoundingBox originalBou private static void CheckGeometries( APrimitive[] primitives, - List truthVertices, - List truthIndices, + List> truthVertices, + List> truthIndices, List originalBoundingBoxes ) { - Assert.That(originalBoundingBoxes.ToArray(), Has.Length.EqualTo(primitives.Length)); + Assert.Multiple(() => + { + Assert.That(originalBoundingBoxes.ToArray(), Has.Length.EqualTo(primitives.Length)); + Assert.That(truthVertices.ToArray(), Has.Length.EqualTo(primitives.Length)); + Assert.That(truthIndices.ToArray(), Has.Length.EqualTo(primitives.Length)); + }); for (int i = 0; i < primitives.Length; i++) { APrimitive? primitive = primitives[i]; Mesh? mesh = ToMesh(primitive); if (mesh != null) { - CheckMeshList(mesh.Vertices, truthVertices, mesh.Indices, truthIndices); + CheckMeshList(mesh.Vertices, truthVertices[i], mesh.Indices, truthIndices[i]); } else { @@ -152,39 +202,304 @@ APrimitive[] primitives } } + private static List> GenVerticesTruth(List optimizersExpectedToRun) + { + var verticesTruth = new List>(); + foreach (var item in optimizersExpectedToRun) + { + verticesTruth.AddRange( + (item != null) + ? item.GetVerticesTruth() + : + [ + [] + ] + ); + } + + return verticesTruth; + } + + private static List> GenIndicesTruth(List optimizersExpectedToRun) + { + var indicesTruth = new List>(); + foreach (var item in optimizersExpectedToRun) + { + indicesTruth.AddRange( + (item != null) + ? item.GetIndicesTruth() + : + [ + [] + ] + ); + } + + return indicesTruth; + } + + private static List GenBoundingBoxesTruth( + List<(BoundingBox bbox, int copies)> boundingBoxAndNumCopiesList + ) + { + var listOfExpectedBoundingBoxes = new List(); + foreach (var entry in boundingBoxAndNumCopiesList) + { + for (int i = 0; i < entry.copies; i++) + { + listOfExpectedBoundingBoxes.Add(entry.bbox); + } + } + + return listOfExpectedBoundingBoxes; + } + + private static List<(int i1, int i2)> GenInstancePairsTruth( + List<(int i1, int i2)> instancePairsBeforeOptimization, + int numPrimitivesPerMeshSplit + ) + { + var instancePairs = new List<(int i1, int i2)>(); + + foreach (var pair in instancePairsBeforeOptimization) + { + for (int i = 0; i < numPrimitivesPerMeshSplit; i++) + { + // + // For example, if we have the following situation + // + // Instance ID before optimization Instance ID form after optimization + // ----------------------------------------------------------------------------- + // 1 X(A1)(B1)XX(C1) + // 1 X(A2)(B2)XX(C2) + // 2 X(A3)(B3)XX(C3) + // 2 X(A4)(B4)XX(C4) + // 2 X(A5)(B5)XX(C5) + // 3 X(A6)(B6)XX(C6) + // X X + // + // X: non-instancing primitive, An, Bn, Cn: instance IDs of first, second, and third instance primitives, respectively + // + // We then want A1 = A2, B1 = B2, C1 = C2. Further, we want A3 = A4, etc., and A3 = A5, etc. Hence, we create pairs + // between A's, between B's, and between C's, where instances before optimization pair up. Since the instance IDs + // after optimization become a flat array, we need to calculate the indices of An that correspond to indices for + // Am, by: + // In = i1 * N + i, + // Im = i2 * N + i, + // where i1 and i2 are instances of meshes with same instance ID, before optimization, N is the number of primitives generated + // for each instanced mesh, and i is the local index into Instance IDs after optimization. Similar calculations hold for B + // and C. + // + // We will also include connections between non-instanced primitives and just ignore those later. + // + int I1 = pair.i1 * numPrimitivesPerMeshSplit + i; + int I2 = pair.i2 * numPrimitivesPerMeshSplit + i; + instancePairs.Add((I1, I2)); + } + } + + return instancePairs; + } + + private static void CheckInstanceIdAssignment( + List<(int i1, int i2)> instancePairsAfterOptimization, + CadRevealNode nodeToCheck, + int preOptimizationPrimitivesCount, + List numPrimitivesPerMeshSplit + ) + { + // Check that the split parts that belong to the same instance have the same instance IDs + foreach (var instancePair in instancePairsAfterOptimization) + { + APrimitive g1 = nodeToCheck.Geometries[instancePair.i1]; + APrimitive g2 = nodeToCheck.Geometries[instancePair.i2]; + if (g1 is InstancedMesh m1 && g2 is InstancedMesh m2) + { + Assert.That(m1.InstanceId, Is.EqualTo(m2.InstanceId)); + } + } + + // Check that the mesh parts that belonged to one part before optimization do NOT have the same instance IDs + int startIndex = 0; + for (int i = 0; i < preOptimizationPrimitivesCount; i++) + { + for (int i1 = 0; i1 < numPrimitivesPerMeshSplit[i]; i1++) + { + for (int i2 = i1 + 1; i2 < numPrimitivesPerMeshSplit[i]; i2++) + { + APrimitive g1 = nodeToCheck.Geometries[startIndex + i1]; + APrimitive g2 = nodeToCheck.Geometries[startIndex + i2]; + if (g1 is InstancedMesh m1 && g2 is InstancedMesh m2) + { + Assert.That(m1.InstanceId, Is.Not.EqualTo(m2.InstanceId)); + } + } + } + + startIndex += numPrimitivesPerMeshSplit[i]; + } + } + + private ( + ScaffoldOptimizer optimizer, + List scaffoldOptimizers + ) ConfigureOptimizerForTesting() + { + // Create optimizers + var scaffoldOptimizers = new List + { + new ScaffoldPartOptimizerTestPartA(), + new ScaffoldPartOptimizerTestPartB(), + new ScaffoldPartOptimizerTestPartC() + }; + + // Configure the optimizer for testing + var optimizer = new ScaffoldOptimizer(); + foreach (var scaffoldOptimizer in scaffoldOptimizers) + { + optimizer.AddPartOptimizer(scaffoldOptimizer); + } + + return (optimizer, scaffoldOptimizers); + } + [Test] - public void CheckScaffoldOptimizerActivation_GivenTestPartOptimizers_VerifyingTheReturnedNode() + [TestCase( + "TestNode, Test A test", + new int[] + { + (int)EOptimizers.A, + (int)EOptimizers.A, + (int)EOptimizers.A, + (int)EOptimizers.A, + (int)EOptimizers.None + }, + new int[] { 1, 1, 1, 1, 1 } + )] + [TestCase( + "TestNode, Test B test", + new int[] + { + (int)EOptimizers.B, + (int)EOptimizers.B, + (int)EOptimizers.B, + (int)EOptimizers.B, + (int)EOptimizers.None + }, + new int[] { 1, 1, 1, 1, 1 } + )] + [TestCase( + "TestNode, Another BTest test", + new int[] + { + (int)EOptimizers.B, + (int)EOptimizers.B, + (int)EOptimizers.B, + (int)EOptimizers.B, + (int)EOptimizers.None + }, + new int[] { 1, 1, 1, 1, 1 } + )] + [TestCase( + "Test C", + new int[] + { + (int)EOptimizers.C, + (int)EOptimizers.C, + (int)EOptimizers.C, + (int)EOptimizers.C, + (int)EOptimizers.None + }, + new int[] { 6, 6, 6, 6, 1 } + )] + public void CheckScaffoldOptimizerActivation_GivenTestPartOptimizers_VerifyingTheReturnedNode( + string partName, + int[] indicesOfOptimizersExpectedToRun, + int[] boundingBoxCopiesInOptimization + ) { ulong currentInstanceId = 100; - var onRequestNewInstanceId = () => currentInstanceId++; // Set up the input - CadRevealNode nodeA = CreateCadRevealNode("TestNode, Test A test").node; - CadRevealNode nodeB = CreateCadRevealNode("TestNode, Test B test").node; - CadRevealNode nodeC = CreateCadRevealNode("TestNode, Another BTest test").node; - (CadRevealNode nodeD, List nodeDMeshes, List boundingBoxes) = CreateCadRevealNode( - "TestNode test" + var input = CreateCadRevealNode(partName, ETestPurpose.TestGeometryAssignment); + + // Configure optimizer for testing + var testOptimizer = ConfigureOptimizerForTesting(); + + // Invoke the optimizer + testOptimizer.optimizer.OptimizeNode(input.node, OnRequestNewInstanceId); + + // Generate the expected results + List optimizersExpectedToRun = indicesOfOptimizersExpectedToRun + .Select(index => (index >= 0) ? testOptimizer.scaffoldOptimizers[index] : null) + .ToList(); + var optimizedNodeAVerticesTruth = GenVerticesTruth(optimizersExpectedToRun); + var optimizedNodeAIndicesTruth = GenIndicesTruth(optimizersExpectedToRun); + var boundingBoxCopiesInOptimizationDef = boundingBoxCopiesInOptimization.Select( + (n, i) => (input.boundingBoxes[i], n) ); + var optimizedNodeEBoundingBoxesTruth = GenBoundingBoxesTruth(boundingBoxCopiesInOptimizationDef.ToList()); - // Create two optimizers - var optimizerA = new ScaffoldPartOptimizerTestPartA(); - var optimizerB = new ScaffoldPartOptimizerTestPartB(); + // Check that the geometries match expected results + CheckGeometries( + input.node.Geometries, + optimizedNodeAVerticesTruth, + optimizedNodeAIndicesTruth, + optimizedNodeEBoundingBoxesTruth + ); - // Configure the optimizer for testing - var optimizer = new ScaffoldOptimizer(); - optimizer.AddPartOptimizer(optimizerA); - optimizer.AddPartOptimizer(optimizerB); + return; + ulong OnRequestNewInstanceId() => currentInstanceId++; + } + + [Test] + public void CheckScaffoldOptimizer_GivenTestPartOptimizers_VerifyingThatNonInstancePrimitivesAreNotAltered() + { + ulong currentInstanceId = 100; + + // Set up the input + var input = CreateCadRevealNode("TestNode test", ETestPurpose.TestGeometryAssignment); + + // Configure optimizer for testing + var testOptimizer = ConfigureOptimizerForTesting(); // Invoke the optimizer - optimizer.OptimizeNode(nodeA, onRequestNewInstanceId); - optimizer.OptimizeNode(nodeB, onRequestNewInstanceId); - optimizer.OptimizeNode(nodeC, onRequestNewInstanceId); - optimizer.OptimizeNode(nodeD, onRequestNewInstanceId); - - // Check the results - CheckGeometries(nodeA.Geometries, optimizerA.GetVerticesTruth(), optimizerA.GetIndicesTruth(), boundingBoxes); - CheckGeometries(nodeB.Geometries, optimizerB.GetVerticesTruth(), optimizerB.GetIndicesTruth(), boundingBoxes); - CheckGeometries(nodeC.Geometries, optimizerB.GetVerticesTruth(), optimizerB.GetIndicesTruth(), boundingBoxes); - CheckThatPrimitivesHaveNotChanged(nodeDMeshes, boundingBoxes, nodeD.Geometries); + testOptimizer.optimizer.OptimizeNode(input.node, OnRequestNewInstanceId); + + // Check that the geometries match expected results + CheckThatPrimitivesHaveNotChanged(input.nodeMeshes, input.boundingBoxes, input.node.Geometries); + + return; + ulong OnRequestNewInstanceId() => currentInstanceId++; + } + + [Test] + public void CheckScaffoldOptimizer_GivenTestPartOptimizers_VerifyingInstanceIds() + { + ulong currentInstanceId = 100; + + // Set up the input + var input = CreateCadRevealNode("Test C", ETestPurpose.TestInstancing); + int preOptimizationPrimitivesCount = input.node.Geometries.Length; + + // Configure optimizer for testing + var testOptimizer = ConfigureOptimizerForTesting(); + + // Invoke the optimizer + testOptimizer.optimizer.OptimizeNode(input.node, OnRequestNewInstanceId); + + // Generate expected results + var optimizedNodeInstancePairsTruth = GenInstancePairsTruth(input.instancePairs, 6); + + // Check that instance IDs are correctly assigned + CheckInstanceIdAssignment( + optimizedNodeInstancePairsTruth, + input.node, + preOptimizationPrimitivesCount, + [6, 6, 6, 6, 6, 6, 1] + ); + + return; + ulong OnRequestNewInstanceId() => currentInstanceId++; } } diff --git a/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTest.cs b/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTest.cs index 25d73336..985491e0 100644 --- a/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTest.cs +++ b/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTest.cs @@ -15,6 +15,6 @@ Func requestChildPartInstanceId ); public abstract string[] GetPartNameTriggerKeywords(); - public abstract List GetVerticesTruth(); - public abstract List GetIndicesTruth(); + public abstract List> GetVerticesTruth(); + public abstract List> GetIndicesTruth(); } diff --git a/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTestPartA.cs b/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTestPartA.cs index 841814a6..07936147 100644 --- a/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTestPartA.cs +++ b/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTestPartA.cs @@ -7,14 +7,20 @@ namespace CadRevealFbxProvider.Tests.BatchUtils.ScaffoldPartOptimizers; public class ScaffoldPartOptimizerTestPartA : ScaffoldPartOptimizerTest { - public override List GetVerticesTruth() + public override List> GetVerticesTruth() { - return [new Vector3(1.0f, 0.0f, 0.0f), new Vector3(0.0f, 1.0f, 0.0f), new Vector3(0.0f, 0.0f, 1.0f)]; + return + [ + [new Vector3(1.0f, 0.0f, 0.0f), new Vector3(0.0f, 1.0f, 0.0f), new Vector3(0.0f, 0.0f, 1.0f)] + ]; } - public override List GetIndicesTruth() + public override List> GetIndicesTruth() { - return [1, 0, 2]; + return + [ + [1, 0, 2] + ]; } public override string Name @@ -32,7 +38,7 @@ Func requestChildPartInstanceId [ new ScaffoldOptimizerResult( basePrimitive, - new Mesh(GetVerticesTruth().ToArray(), GetIndicesTruth().ToArray(), mesh.Error), + new Mesh(GetVerticesTruth()[0].ToArray(), GetIndicesTruth()[0].ToArray(), mesh.Error), 0, requestChildPartInstanceId ) diff --git a/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTestPartB.cs b/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTestPartB.cs index 2ea0a1f9..fe8066ca 100644 --- a/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTestPartB.cs +++ b/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTestPartB.cs @@ -7,14 +7,20 @@ namespace CadRevealFbxProvider.Tests.BatchUtils.ScaffoldPartOptimizers; public class ScaffoldPartOptimizerTestPartB : ScaffoldPartOptimizerTest { - public override List GetVerticesTruth() + public override List> GetVerticesTruth() { - return [new Vector3(3.0f, 0.0f, 0.0f), new Vector3(0.0f, 4.0f, 0.0f), new Vector3(0.0f, 0.0f, 5.0f)]; + return + [ + [new Vector3(3.0f, 0.0f, 0.0f), new Vector3(0.0f, 4.0f, 0.0f), new Vector3(0.0f, 0.0f, 5.0f)] + ]; } - public override List GetIndicesTruth() + public override List> GetIndicesTruth() { - return [2, 0, 1]; + return + [ + [2, 0, 1] + ]; } public override string Name @@ -32,7 +38,7 @@ Func requestChildPartInstanceId [ new ScaffoldOptimizerResult( basePrimitive, - new Mesh(GetVerticesTruth().ToArray(), GetIndicesTruth().ToArray(), mesh.Error), + new Mesh(GetVerticesTruth()[0].ToArray(), GetIndicesTruth()[0].ToArray(), mesh.Error), 0, requestChildPartInstanceId ) diff --git a/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTestPartC.cs b/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTestPartC.cs new file mode 100644 index 00000000..488db4cc --- /dev/null +++ b/CadRevealFbxProvider.Tests/BatchUtils/ScaffoldPartOptimizers/ScaffoldPartOptimizerTestPartC.cs @@ -0,0 +1,102 @@ +namespace CadRevealFbxProvider.Tests.BatchUtils.ScaffoldPartOptimizers; + +using System.Drawing; +using System.Numerics; +using CadRevealComposer; +using CadRevealComposer.Primitives; +using CadRevealComposer.Tessellation; +using CadRevealFbxProvider.BatchUtils.ScaffoldPartOptimizers; + +public class ScaffoldPartOptimizerTestPartC : ScaffoldPartOptimizerTest +{ + public override List> GetVerticesTruth() + { + return + [ + [], + [new Vector3(3.0f, 0.0f, 0.0f), new Vector3(0.0f, 4.0f, 0.0f), new Vector3(0.0f, 0.0f, 5.0f)], + [new Vector3(3.0f, 0.0f, 0.0f), new Vector3(0.0f, 4.0f, 0.0f), new Vector3(0.0f, 0.0f, 5.0f)], + [], + [new Vector3(3.0f, 0.0f, 0.0f), new Vector3(0.0f, 4.0f, 0.0f), new Vector3(0.0f, 0.0f, 5.0f)], + [new Vector3(3.0f, 0.0f, 0.0f), new Vector3(0.0f, 4.0f, 0.0f), new Vector3(0.0f, 0.0f, 5.0f)] + ]; + } + + public override List> GetIndicesTruth() + { + return + [ + [], + [2, 0, 1], + [2, 0, 1], + [], + [2, 0, 1], + [2, 0, 1] + ]; + } + + public override string Name + { + get { return "Part C test optimizer"; } + } + + public override IScaffoldOptimizerResult[] Optimize( + APrimitive basePrimitive, + Mesh mesh, + Func requestChildPartInstanceId + ) + { + return + [ + new ScaffoldOptimizerResult( + new Circle( + Matrix4x4.Identity, + new Vector3(0, 0, 1), + 0, + Color.Black, + new BoundingBox(new Vector3(0, 0, 0), new Vector3(1, 1, 1)) + ) + ), + new ScaffoldOptimizerResult( + basePrimitive, + new Mesh(GetVerticesTruth()[1].ToArray(), GetIndicesTruth()[1].ToArray(), mesh.Error), + 0, + requestChildPartInstanceId + ), + new ScaffoldOptimizerResult( + basePrimitive, + new Mesh(GetVerticesTruth()[2].ToArray(), GetIndicesTruth()[2].ToArray(), mesh.Error), + 1, + requestChildPartInstanceId + ), + new ScaffoldOptimizerResult( + new Circle( + Matrix4x4.Identity, + new Vector3(0, 0, 1), + 0, + Color.Black, + new BoundingBox(new Vector3(0, 0, 0), new Vector3(1, 1, 1)) + ) + ), + new ScaffoldOptimizerResult( + new TriangleMesh( + new Mesh(GetVerticesTruth()[4].ToArray(), GetIndicesTruth()[4].ToArray(), mesh.Error), + 0, + Color.Black, + new BoundingBox(new Vector3(0, 0, 0), new Vector3(1, 1, 1)) + ) + ), + new ScaffoldOptimizerResult( + basePrimitive, + new Mesh(GetVerticesTruth()[5].ToArray(), GetIndicesTruth()[5].ToArray(), mesh.Error), + 2, + requestChildPartInstanceId + ) + ]; + } + + public override string[] GetPartNameTriggerKeywords() + { + return ["Test C"]; + } +}