Compare commits

...

1 Commits

Author SHA1 Message Date
Jonathon Broughton a1462813d5 Improves instance handling for Navisworks sends
This change introduces instance definition caching and reuse during Navisworks sends.
It significantly optimizes performance for models with many instances by creating references to shared definitions.

A new `CreateInstanceReference` method constructs lightweight instance objects that reference the converted definition and store instance-specific transforms.
The `GetInstanceTransform` method extracts transformation matrices from Navisworks instances.
2025-09-26 13:11:06 +02:00
6 changed files with 238 additions and 36 deletions
@@ -112,10 +112,49 @@ public class NavisworksRootObjectBuilder(
int processedCount = 0;
int totalCount = navisworksModelItems.Count;
// Store instance definitions by their definition ID (hash of the first instance)
Dictionary<int, Base> instanceDefinitions = [];
foreach (var item in navisworksModelItems)
{
cancellationToken.ThrowIfCancellationRequested();
string applicationId = elementSelectionService.GetModelItemPath(item);
if (item.InstanceHashCode != 0)
{
var instanceDefinitionId = item.Instances.First.GetHashCode();
// If this instance definition is already converted, create an instance reference
if (instanceDefinitions.TryGetValue(instanceDefinitionId, out var definitionBase))
{
var instanceBase = CreateInstanceReference(definitionBase, item);
convertedBases[applicationId] = instanceBase;
results.Add(new SendConversionResult(Status.SUCCESS, applicationId, item.GetType().Name, instanceBase));
processedCount++;
onOperationProgressed.Report(new CardProgress("Converting", (double)processedCount / totalCount));
continue;
}
}
// Convert the item (either a new instance definition or non-instance item)
var converted = ConvertNavisworksItem(item, convertedBases, projectId);
if (converted.Status == Status.SUCCESS)
{
results.Add(converted);
// If this is an instance definition, store it for reuse
if (item.InstanceHashCode != 0)
{
var instanceDefinitionId = item.Instances.First.GetHashCode();
instanceDefinitions[instanceDefinitionId] = convertedBases[converted.SourceId]!;
}
processedCount++;
onOperationProgressed.Report(new CardProgress("Converting", (double)processedCount / totalCount));
continue;
}
results.Add(converted);
processedCount++;
onOperationProgressed.Report(new CardProgress("Converting", (double)processedCount / totalCount));
@@ -291,6 +330,84 @@ public class NavisworksRootObjectBuilder(
return Task.CompletedTask;
}
/// <summary>
/// Creates an instance reference that points to a converted base definition with instance-specific transforms.
/// </summary>
/// <param name="definitionBase">The converted base definition to reference.</param>
/// <param name="instanceItem">The Navisworks instance item.</param>
/// <returns>A Base object representing the instance with transforms.</returns>
private Base CreateInstanceReference(Base definitionBase, NAV.ModelItem instanceItem)
{
// Get the instance transform from Navisworks
var instanceTransform = GetInstanceTransform(instanceItem);
// Create a new base that references the definition
var instanceBase = new Base
{
applicationId = elementSelectionService.GetModelItemPath(instanceItem),
// Store reference to the definition
["@definition"] = definitionBase.applicationId,
// Store the instance transform
["transform"] = instanceTransform,
// Copy the display value (geometry) from the definition
["displayValue"] = definitionBase["displayValue"],
// Copy other properties from definition if needed
["properties"] = definitionBase["properties"]
};
return instanceBase;
}
/// <summary>
/// Extracts the transform matrix from a Navisworks instance item.
/// </summary>
/// <param name="instanceItem">The Navisworks instance item.</param>
/// <returns>A transform matrix as a flat array or null if no transform.</returns>
private double[]? GetInstanceTransform(NAV.ModelItem instanceItem)
{
try
{
// Get the transform from the instance
var transform = instanceItem.Transform;
if (transform == null)
{
return null;
}
// Convert the Navisworks transform to a flat array
// For now, return identity matrix as POC - proper transform extraction needs Navisworks API research
var matrix = new double[16];
// Identity matrix
matrix[0] = 1;
matrix[1] = 0;
matrix[2] = 0;
matrix[3] = 0;
matrix[4] = 0;
matrix[5] = 1;
matrix[6] = 0;
matrix[7] = 0;
matrix[8] = 0;
matrix[9] = 0;
matrix[10] = 1;
matrix[11] = 0;
matrix[12] = 0;
matrix[13] = 0;
matrix[14] = 0;
matrix[15] = 1;
return matrix;
}
catch (Exception ex) when (!ex.IsFatal())
{
logger.LogWarning(
ex,
"Failed to extract transform from instance {id}",
elementSelectionService.GetModelItemPath(instanceItem)
);
return null;
}
}
/// <summary>
/// Converts a single Navisworks item to a Speckle object.
/// </summary>
@@ -33,6 +33,9 @@ public class DisplayValueExtractor
return [];
}
return !IsElementVisible(modelItem) ? [] : _geometryConverter.Convert(modelItem);
// Check if this is an instance definition by looking at InstanceHashCode
bool isInstanceDefinition = modelItem.InstanceHashCode != 0;
return !IsElementVisible(modelItem) ? [] : _geometryConverter.Convert(modelItem, isInstanceDefinition);
}
}
@@ -0,0 +1,44 @@
using Microsoft.Extensions.Logging;
using Speckle.Converter.Navisworks.Helpers;
using static Speckle.Converter.Navisworks.Helpers.ElementSelectionHelper;
namespace Speckle.Converter.Navisworks.Helpers;
/// <summary>
/// Helper class for extracting and working with Navisworks transforms.
/// </summary>
public static class NavisworksTransformHelper
{
/// <summary>
/// Extracts the transform matrix from a Navisworks instance item.
/// </summary>
/// <param name="instanceItem">The Navisworks instance item.</param>
/// <returns>A transform matrix as a flat array or null if no transform.</returns>
public static double[]? GetInstanceTransform(NAV.ModelItem instanceItem)
{
try
{
// Get the transform from the instance
var transform = instanceItem.Transform;
if (transform == null)
{
return null;
}
// Convert the Navisworks transform to a flat array
var matrix = new double[16];
matrix[0] = transform.M00; matrix[1] = transform.M01; matrix[2] = transform.M02; matrix[3] = transform.M03;
matrix[4] = transform.M10; matrix[5] = transform.M11; matrix[6] = transform.M12; matrix[7] = transform.M13;
matrix[8] = transform.M20; matrix[9] = transform.M21; matrix[10] = transform.M22; matrix[11] = transform.M23;
matrix[12] = transform.M30; matrix[13] = transform.M31; matrix[14] = transform.M32; matrix[15] = transform.M33;
return matrix;
}
catch (Exception ex) when (!ex.IsFatal())
{
// Note: We can't use logger here since this is a static method
// The calling code should handle logging if needed
return null;
}
}
}
@@ -50,4 +50,5 @@ public class NavisworksRootToSpeckleConverter : IRootToSpeckleConverter
return result;
}
}
@@ -39,7 +39,7 @@ public class GeometryToSpeckleConverter
/// Converts a ModelItem's geometry to Speckle display geometry by accessing the underlying COM objects.
/// Applies necessary transformations and unit scaling.
/// </summary>
internal List<Base> Convert(NAV.ModelItem modelItem)
internal List<Base> Convert(NAV.ModelItem modelItem, bool isInstanceDefinition = false)
{
if (modelItem == null)
{
@@ -64,7 +64,7 @@ public class GeometryToSpeckleConverter
CollectFragments(path, fragmentStack);
}
return ProcessFragments(fragmentStack, paths);
return ProcessFragments(fragmentStack, paths, isInstanceDefinition);
}
finally
{
@@ -103,7 +103,7 @@ public class GeometryToSpeckleConverter
}
}
private List<Base> ProcessFragments(Stack<InwOaFragment3> fragmentStack, InwSelectionPathsColl paths)
private List<Base> ProcessFragments(Stack<InwOaFragment3> fragmentStack, InwSelectionPathsColl paths, bool isInstanceDefinition = false)
{
var callbackListeners = new List<PrimitiveProcessor>();
@@ -132,7 +132,7 @@ public class GeometryToSpeckleConverter
callbackListeners.Add(processor);
}
var baseGeometries = ProcessGeometries(callbackListeners);
var baseGeometries = ProcessGeometries(callbackListeners, isInstanceDefinition);
return baseGeometries;
}
@@ -147,7 +147,7 @@ public class GeometryToSpeckleConverter
return IsSameFragmentPath(fragmentPathData, pathData);
}
private List<Base> ProcessGeometries(List<PrimitiveProcessor> processors)
private List<Base> ProcessGeometries(List<PrimitiveProcessor> processors, bool isInstanceDefinition = false)
{
var baseGeometries = new List<Base>();
@@ -155,13 +155,13 @@ public class GeometryToSpeckleConverter
{
if (processor.Triangles.Count > 0)
{
var mesh = CreateMesh(processor.Triangles);
var mesh = CreateMesh(processor.Triangles, isInstanceDefinition);
baseGeometries.Add(mesh);
}
if (processor.Lines.Count > 0)
{
var lines = CreateLines(processor.Lines);
var lines = CreateLines(processor.Lines, isInstanceDefinition);
baseGeometries.AddRange(lines);
}
}
@@ -169,7 +169,7 @@ public class GeometryToSpeckleConverter
return baseGeometries;
}
private Mesh CreateMesh(IReadOnlyList<SafeTriangle> triangles)
private Mesh CreateMesh(IReadOnlyList<SafeTriangle> triangles, bool isInstanceDefinition = false)
{
var vertices = new List<double>();
var faces = new List<int>();
@@ -178,20 +178,40 @@ public class GeometryToSpeckleConverter
{
var triangle = triangles[t];
// No need to worry about disposal of COM across boundaries - we're working with our safe structs
vertices.AddRange(
[
(triangle.Vertex1.X + _transformVector.X) * SCALE,
(triangle.Vertex1.Y + _transformVector.Y) * SCALE,
(triangle.Vertex1.Z + _transformVector.Z) * SCALE,
(triangle.Vertex2.X + _transformVector.X) * SCALE,
(triangle.Vertex2.Y + _transformVector.Y) * SCALE,
(triangle.Vertex2.Z + _transformVector.Z) * SCALE,
(triangle.Vertex3.X + _transformVector.X) * SCALE,
(triangle.Vertex3.Y + _transformVector.Y) * SCALE,
(triangle.Vertex3.Z + _transformVector.Z) * SCALE
]
);
// For instance definitions, don't apply global transform - only apply coordinate system and scaling
if (isInstanceDefinition)
{
vertices.AddRange(
[
triangle.Vertex1.X * SCALE,
triangle.Vertex1.Y * SCALE,
triangle.Vertex1.Z * SCALE,
triangle.Vertex2.X * SCALE,
triangle.Vertex2.Y * SCALE,
triangle.Vertex2.Z * SCALE,
triangle.Vertex3.X * SCALE,
triangle.Vertex3.Y * SCALE,
triangle.Vertex3.Z * SCALE
]
);
}
else
{
// For non-instance geometry, apply global transform as before
vertices.AddRange(
[
(triangle.Vertex1.X + _transformVector.X) * SCALE,
(triangle.Vertex1.Y + _transformVector.Y) * SCALE,
(triangle.Vertex1.Z + _transformVector.Z) * SCALE,
(triangle.Vertex2.X + _transformVector.X) * SCALE,
(triangle.Vertex2.Y + _transformVector.Y) * SCALE,
(triangle.Vertex2.Z + _transformVector.Z) * SCALE,
(triangle.Vertex3.X + _transformVector.X) * SCALE,
(triangle.Vertex3.Y + _transformVector.Y) * SCALE,
(triangle.Vertex3.Z + _transformVector.Z) * SCALE
]
);
}
faces.AddRange([3, t * 3, t * 3 + 1, t * 3 + 2]);
}
@@ -203,23 +223,37 @@ public class GeometryToSpeckleConverter
};
}
private List<Line> CreateLines(IReadOnlyList<SafeLine> lines) =>
private List<Line> CreateLines(IReadOnlyList<SafeLine> lines, bool isInstanceDefinition = false) =>
(
from line in lines
select new Line
{
start = new Point(
(line.Start.X + _transformVector.X) * SCALE,
(line.Start.Y + _transformVector.Y) * SCALE,
(line.Start.Z + _transformVector.Z) * SCALE,
_settings.Derived.SpeckleUnits
),
end = new Point(
(line.End.X + _transformVector.X) * SCALE,
(line.End.Y + _transformVector.Y) * SCALE,
(line.End.Z + _transformVector.Z) * SCALE,
_settings.Derived.SpeckleUnits
),
start = isInstanceDefinition
? new Point(
line.Start.X * SCALE,
line.Start.Y * SCALE,
line.Start.Z * SCALE,
_settings.Derived.SpeckleUnits
)
: new Point(
(line.Start.X + _transformVector.X) * SCALE,
(line.Start.Y + _transformVector.Y) * SCALE,
(line.Start.Z + _transformVector.Z) * SCALE,
_settings.Derived.SpeckleUnits
),
end = isInstanceDefinition
? new Point(
line.End.X * SCALE,
line.End.Y * SCALE,
line.End.Z * SCALE,
_settings.Derived.SpeckleUnits
)
: new Point(
(line.End.X + _transformVector.X) * SCALE,
(line.End.Y + _transformVector.Y) * SCALE,
(line.End.Z + _transformVector.Z) * SCALE,
_settings.Derived.SpeckleUnits
),
units = _settings.Derived.SpeckleUnits
}
).ToList();
@@ -40,6 +40,7 @@ public class ModelItemToToSpeckleConverter : IToSpeckleTopLevelConverter
public Base Convert(object target) =>
target == null ? throw new ArgumentNullException(nameof(target)) : Convert((NAV.ModelItem)target);
// Converts a Navisworks ModelItem into a Speckle Base object
private Base Convert(NAV.ModelItem target)
{
@@ -51,6 +52,7 @@ public class ModelItemToToSpeckleConverter : IToSpeckleTopLevelConverter
: CreateNonGeometryObject(target, name, handler);
}
// There are in fact only two types of objects: geometry and non-geometry, the latter being collections of other objects
private NavisworksObject CreateGeometryObject(NAV.ModelItem target, string name, IPropertyHandler propertyHandler) =>
new()
@@ -61,6 +63,7 @@ public class ModelItemToToSpeckleConverter : IToSpeckleTopLevelConverter
units = _settingsStore.Current.Derived.SpeckleUnits,
};
private Collection CreateNonGeometryObject(NAV.ModelItem target, string name, IPropertyHandler propertyHandler) =>
new()
{