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
19 changed files with 362 additions and 415 deletions
@@ -20,8 +20,23 @@ public class CsiFrameSectionPropertyExtractor : IFrameSectionPropertyExtractor
_settingsStore = settingsStore;
}
public void ExtractProperties(string sectionName, Dictionary<string, object?> properties) =>
public void ExtractProperties(string sectionName, Dictionary<string, object?> properties)
{
GetMaterialName(sectionName, properties);
GetSectionProperties(sectionName, properties);
GetPropertyModifiers(sectionName, properties);
}
private void GetMaterialName(string sectionName, Dictionary<string, object?> properties)
{
// get material name
string materialName = string.Empty;
_settingsStore.Current.SapModel.PropFrame.GetMaterial(sectionName, ref materialName);
// append to General Data of properties dictionary
Dictionary<string, object?> generalData = properties.EnsureNested(SectionPropertyCategory.GENERAL_DATA);
generalData["Material"] = materialName;
}
private void GetSectionProperties(string sectionName, Dictionary<string, object?> properties)
{
@@ -54,20 +69,47 @@ public class CsiFrameSectionPropertyExtractor : IFrameSectionPropertyExtractor
ref radiusOfGyrationAboutMinorAxis
);
string distanceUnit = _settingsStore.Current.SpeckleUnits;
string areaUnit = $"{distanceUnit}²"; // // TODO: Formalize this better
string modulusUnit = $"{distanceUnit}³"; // // TODO: Formalize this better
string inertiaUnit = $"{distanceUnit}\u2074"; // TODO: Formalize this better
Dictionary<string, object?> mechanicalProperties = properties.EnsureNested(
SectionPropertyCategory.SECTION_PROPERTIES
);
mechanicalProperties.Add("Area", crossSectionalArea);
mechanicalProperties.Add("As2", shearAreaInMajorAxisDirection);
mechanicalProperties.Add("As3", shearAreaInMinorAxisDirection);
mechanicalProperties.Add("J", torsionalConstant);
mechanicalProperties.Add("I22", momentOfInertiaAboutMajorAxis);
mechanicalProperties.Add("I33", momentOfInertiaAboutMinorAxis);
mechanicalProperties.Add("S22", sectionModulusAboutMajorAxis);
mechanicalProperties.Add("S33", sectionModulusAboutMinorAxis);
mechanicalProperties.Add("Z22", plasticModulusAboutMajorAxis);
mechanicalProperties.Add("Z33", plasticModulusAboutMinorAxis);
mechanicalProperties.Add("R22", radiusOfGyrationAboutMajorAxis);
mechanicalProperties.Add("R33", radiusOfGyrationAboutMinorAxis);
mechanicalProperties.AddWithUnits("Area", crossSectionalArea, areaUnit);
mechanicalProperties.AddWithUnits("As2", shearAreaInMajorAxisDirection, areaUnit);
mechanicalProperties.AddWithUnits("As3", shearAreaInMinorAxisDirection, areaUnit);
mechanicalProperties.AddWithUnits("J", torsionalConstant, inertiaUnit);
mechanicalProperties.AddWithUnits("I22", momentOfInertiaAboutMajorAxis, inertiaUnit);
mechanicalProperties.AddWithUnits("I33", momentOfInertiaAboutMinorAxis, inertiaUnit);
mechanicalProperties.AddWithUnits("S22", sectionModulusAboutMajorAxis, modulusUnit);
mechanicalProperties.AddWithUnits("S33", sectionModulusAboutMinorAxis, modulusUnit);
mechanicalProperties.AddWithUnits("Z22", plasticModulusAboutMajorAxis, modulusUnit);
mechanicalProperties.AddWithUnits("Z33", plasticModulusAboutMinorAxis, modulusUnit);
mechanicalProperties.AddWithUnits("R22", radiusOfGyrationAboutMajorAxis, distanceUnit);
mechanicalProperties.AddWithUnits("R33", radiusOfGyrationAboutMinorAxis, distanceUnit);
}
private void GetPropertyModifiers(string sectionName, Dictionary<string, object?> properties)
{
double[] stiffnessModifiersArray = [];
_settingsStore.Current.SapModel.PropFrame.GetModifiers(sectionName, ref stiffnessModifiersArray);
Dictionary<string, object?> modifiers =
new()
{
["Cross-section (Axial) Area"] = stiffnessModifiersArray[0],
["Shear Area in 2 Direction"] = stiffnessModifiersArray[1],
["Shear Area in 3 Direction"] = stiffnessModifiersArray[2],
["Torsional Constant"] = stiffnessModifiersArray[3],
["Moment of Inertia about 2 Axis"] = stiffnessModifiersArray[4],
["Moment of Inertia about 3 Axis"] = stiffnessModifiersArray[5],
["Mass"] = stiffnessModifiersArray[6],
["Weight"] = stiffnessModifiersArray[7],
};
Dictionary<string, object?> generalData = properties.EnsureNested(SectionPropertyCategory.GENERAL_DATA);
generalData["Modifiers"] = modifiers;
}
}
@@ -23,7 +23,6 @@ public class CsiResultsExtractorFactory
ResultsKey.PIER_FORCES => _serviceProvider.GetRequiredService<CsiPierForceResultsExtractor>(),
ResultsKey.SPANDREL_FORCES => _serviceProvider.GetRequiredService<CsiSpandrelForceResultsExtractor>(),
ResultsKey.STORY_DRIFTS => _serviceProvider.GetRequiredService<CsiStoryDriftsResultsExtractor>(),
ResultsKey.STORY_FORCES => _serviceProvider.GetRequiredService<CsiStoryForceResultsExtractor>(),
_ => throw new InvalidOperationException($"{resultsKey} not accounted for in CsiResultsExtractorFactory")
};
}
@@ -47,17 +47,18 @@ public class EtabsSectionUnpacker : ISectionUnpacker
string sectionName = entry.Key;
List<string> frameIds = entry.Value;
// initialize properties
Dictionary<string, object?> properties = [];
// Initialize properties outside the if statement
Dictionary<string, object?> properties = new Dictionary<string, object?>();
// Extract properties if valid section name
// "None" is weird but api returns that string if an opening, null element etc.
if (sectionName != "None" && !string.IsNullOrEmpty(sectionName))
// get the properties of the section
// openings will have objects assigned to them, but won't have properties
// sectionName is initialized with string.Empty, but api ref returns string "None"
if (sectionName != "None")
{
properties = _propertyExtractor.ExtractFrameSectionProperties(sectionName);
}
// create section proxy
// create the section proxy
GroupProxy sectionProxy =
new()
{
@@ -65,8 +66,8 @@ public class EtabsSectionUnpacker : ISectionUnpacker
name = sectionName,
applicationId = sectionName,
objects = frameIds,
["type"] = "Frame Section",
["properties"] = properties
["type"] = "Frame Section", // since sectionProxies are a flat list, need some way to distinguish from shell
["properties"] = properties // openings will just have an empty dict here
};
yield return sectionProxy;
@@ -80,8 +81,8 @@ public class EtabsSectionUnpacker : ISectionUnpacker
string sectionName = entry.Key;
List<string> frameIds = entry.Value;
// initialize properties outside the if statement
Dictionary<string, object?> properties = [];
// Initialize properties outside the if statement
Dictionary<string, object?> properties = new Dictionary<string, object?>();
// get the properties of the section
// openings will have objects assigned to them, but won't have properties
@@ -91,7 +92,7 @@ public class EtabsSectionUnpacker : ISectionUnpacker
properties = _propertyExtractor.ExtractShellSectionProperties(sectionName);
}
// create section proxy
// create the section proxy
GroupProxy sectionProxy =
new()
{
@@ -1,5 +1,4 @@
using Speckle.Connectors.CSiShared.HostApp.Helpers;
using Speckle.Connectors.ETABSShared.HostApp.Services;
using Speckle.Converters.Common;
using Speckle.Converters.CSiShared;
using Speckle.Converters.CSiShared.Utils;
@@ -9,96 +8,69 @@ namespace Speckle.Connectors.ETABSShared.HostApp.Helpers;
/// <summary>
/// Extracts ETABS-specific frame section properties.
/// </summary>
/// <remarks>
/// The bulk loading strategy is necessary here because we can't know which database table contains which section
/// beforehand - there are multiple tables like "Frame Section Property Definitions - Steel",
/// "Frame Section Property Definitions - Concrete", etc.
/// </remarks>
public class EtabsFrameSectionPropertyExtractor : IApplicationFrameSectionPropertyExtractor
{
private readonly IConverterSettingsStore<CsiConversionSettings> _settingsStore;
private readonly EtabsSectionPropertyDefinitionService _definitionService;
public EtabsFrameSectionPropertyExtractor(
IConverterSettingsStore<CsiConversionSettings> settingsStore,
EtabsSectionPropertyDefinitionService definitionService
)
public EtabsFrameSectionPropertyExtractor(IConverterSettingsStore<CsiConversionSettings> settingsStore)
{
_settingsStore = settingsStore;
_definitionService = definitionService;
}
/// <summary>
/// Gets frame section properties from preloaded database table data
/// Gets generalised frame section properties
/// </summary>
/// <remarks>
/// Property categorization is done heuristically - order matters in the parsing logic.
/// Sap2000 doesn't support this method, unfortunately
/// Alternative is to account for extraction according to section type - we're talking over 40 section types!
/// This way, we get basic information with minimal computational costs.
/// </remarks>
public void ExtractProperties(string sectionName, Dictionary<string, object?> properties)
{
// get frame definitions from the service (which uses database table extraction)
// this is a fast dictionary lookup since all data is preloaded
if (!_definitionService.FrameDefinitions.TryGetValue(sectionName, out var rawDatabaseTableProperties))
// Get all frame properties
int numberOfNames = 0;
string[] names = [];
eFramePropType[] propTypes = [];
double[] t3 = [],
t2 = [],
tf = [],
tw = [],
t2b = [],
tfb = [],
area = [];
_settingsStore.Current.SapModel.PropFrame.GetAllFrameProperties_2(
ref numberOfNames,
ref names,
ref propTypes,
ref t3,
ref t2,
ref tf,
ref tw,
ref t2b,
ref tfb,
ref area
);
// Find the index of the current section
int sectionIndex = Array.IndexOf(names, sectionName);
if (sectionIndex != -1)
{
return; // no definitions found for this section
}
// General Data
var generalData = properties.EnsureNested(SectionPropertyCategory.GENERAL_DATA);
generalData["Section Shape"] = propTypes[sectionIndex].ToString();
// define table keys that we don't want to include in the section proxy properties
var keysToExclude = new HashSet<string>
{
"GUID",
"Name",
"Color",
"Notes",
"FileName",
"FromFile",
"SectInFile",
"NotAutoFact"
};
// get the section type / shape using the dedicated api query (exception to the database approach)
// this specific property isn't available in the database table extraction
eFramePropType framePropType = 0;
_settingsStore.Current.SapModel.PropFrame.GetTypeOAPI(sectionName, ref framePropType);
Dictionary<string, object?> generalProperties = properties.EnsureNested(SectionPropertyCategory.GENERAL_DATA);
generalProperties.Add("Section Shape", framePropType.ToString());
// heuristic property categorization based on key patterns and parse-ability
// NOTE: this is gross and quite dangerous 🤨 but beats specific frame prop sect. property extractions imo
// order matters here! we check for known string props first, then modifiers, then assume doubles are dimensions
foreach (KeyValuePair<string, string> rawDatabaseTableProperty in rawDatabaseTableProperties)
{
string key = rawDatabaseTableProperty.Key;
string value = rawDatabaseTableProperty.Value;
// skip metadata fields we don't care about
if (!keysToExclude.Contains(key))
{
// material is always a string, grab it first
if (key == "Material")
{
generalProperties.Add(key, value);
}
// modifier properties end with "Mod" and should be numeric
else if (key.EndsWith("Mod") && double.TryParse(value, out double parsedModValue))
{
Dictionary<string, object?> modificationProperties = properties.EnsureNested(
SectionPropertyCategory.MODIFIERS
);
modificationProperties.Add(key, parsedModValue);
}
// anything else that parses as a double is assumed to be a section dimension
// this covers things like t3, t2, tf, tw, area, etc. without having to enumerate them all
else if (double.TryParse(value, out double parsedDimensionValue))
{
Dictionary<string, object?> sectionDimensions = properties.EnsureNested(
SectionPropertyCategory.SECTION_DIMENSIONS
);
sectionDimensions.Add(key, parsedDimensionValue);
}
// if it doesn't parse as double and isn't a known string property, we skip it
// this is acceptable - we'd rather miss some edge case properties than crash
}
// Section Dimensions
string unit = _settingsStore.Current.SpeckleUnits;
var sectionDimensions = properties.EnsureNested(SectionPropertyCategory.SECTION_DIMENSIONS);
sectionDimensions.AddWithUnits("t3", t3[sectionIndex], unit);
sectionDimensions.AddWithUnits("t2", t2[sectionIndex], unit);
sectionDimensions.AddWithUnits("tf", tf[sectionIndex], unit);
sectionDimensions.AddWithUnits("tw", tw[sectionIndex], unit);
sectionDimensions.AddWithUnits("t2b", t2b[sectionIndex], unit);
sectionDimensions.AddWithUnits("tfb", tfb[sectionIndex], unit);
sectionDimensions.AddWithUnits("Area", area[sectionIndex], $"{unit}²");
}
}
}
@@ -1,105 +0,0 @@
using Speckle.Converters.Common;
using Speckle.Converters.CSiShared;
using Speckle.Converters.CSiShared.ToSpeckle.Helpers;
namespace Speckle.Connectors.ETABSShared.HostApp.Services;
/// <summary>
/// Loads and caches section property definitions from database tables for both frame and shell sections.
/// </summary>
public class EtabsSectionPropertyDefinitionService
{
private readonly IConverterSettingsStore<CsiConversionSettings> _settingsStore;
public IReadOnlyDictionary<string, IReadOnlyDictionary<string, string>> FrameDefinitions { get; }
public IReadOnlyDictionary<string, IReadOnlyDictionary<string, string>> ShellDefinitions { get; }
public EtabsSectionPropertyDefinitionService(
DatabaseTableExtractor databaseTableExtractor,
IConverterSettingsStore<CsiConversionSettings> settingsStore
)
{
_settingsStore = settingsStore;
var availableTableKeys = GetAvailableTableKeys();
FrameDefinitions = LoadFrameDefinitions(databaseTableExtractor, availableTableKeys);
ShellDefinitions = LoadShellDefinitions(databaseTableExtractor, availableTableKeys);
}
private static IReadOnlyDictionary<string, IReadOnlyDictionary<string, string>> LoadFrameDefinitions(
DatabaseTableExtractor databaseTableExtractor,
string[] availableTableKeys
)
{
var frameTableKeys = GetFrameSectionPropertyDefinitionTableKeys(availableTableKeys);
return LoadDefinitionsFromTables(databaseTableExtractor, frameTableKeys);
}
private static IReadOnlyDictionary<string, IReadOnlyDictionary<string, string>> LoadShellDefinitions(
DatabaseTableExtractor databaseTableExtractor,
string[] availableTableKeys
)
{
var shellTableKeys = GetShellSectionPropertyDefinitionTableKeys(availableTableKeys);
return LoadDefinitionsFromTables(databaseTableExtractor, shellTableKeys);
}
private static IReadOnlyDictionary<string, IReadOnlyDictionary<string, string>> LoadDefinitionsFromTables(
DatabaseTableExtractor databaseTableExtractor,
IEnumerable<string> tableKeys
)
{
var definitions = new Dictionary<string, IReadOnlyDictionary<string, string>>();
foreach (string tableKey in tableKeys)
{
var tableData = databaseTableExtractor.GetTableData(tableKey, "Name");
foreach (var row in tableData.Rows)
{
definitions[row.Key] = row.Value;
}
}
return definitions;
}
private static IEnumerable<string> GetFrameSectionPropertyDefinitionTableKeys(string[] availableTableKeys)
{
var keysToExclude = new HashSet<string>
{
"Frame Section Property Definitions - Summary",
"Frame Section Property Definitions - Concrete Beam Reinforcing",
"Frame Section Property Definitions - Concrete Column Reinforcing"
};
return availableTableKeys.Where(key =>
key.StartsWith("Frame Section Property Definitions") && !keysToExclude.Contains(key)
);
}
private static IEnumerable<string> GetShellSectionPropertyDefinitionTableKeys(string[] availableTableKeys)
{
var keysToExclude = new HashSet<string> { "Area Section Property Definitions - Summary" };
return availableTableKeys.Where(key =>
key.StartsWith("Area Section Property Definitions") && !keysToExclude.Contains(key)
);
}
private string[] GetAvailableTableKeys()
{
int numberTables = 0;
string[] tableKey = [],
tableName = [];
int[] importType = [];
_ = _settingsStore.Current.SapModel.DatabaseTables.GetAvailableTables(
ref numberTables,
ref tableKey,
ref tableName,
ref importType
);
return tableKey;
}
}
@@ -34,7 +34,7 @@ public class EtabsSectionPropertyExtractor
/// </summary>
public Dictionary<string, object?> ExtractFrameSectionProperties(string sectionName)
{
Dictionary<string, object?> properties = [];
Dictionary<string, object?> properties = new();
_csiFrameExtractor.ExtractProperties(sectionName, properties);
_etabsFrameExtractor.ExtractProperties(sectionName, properties);
return properties;
@@ -45,7 +45,7 @@ public class EtabsSectionPropertyExtractor
/// </summary>
public Dictionary<string, object?> ExtractShellSectionProperties(string sectionName)
{
Dictionary<string, object?> properties = [];
Dictionary<string, object?> properties = new();
_csiShellExtractor.ExtractProperties(sectionName, properties);
_etabsShellExtractor.ExtractProperties(sectionName, properties);
return properties;
@@ -3,7 +3,6 @@ using Speckle.Connectors.CSiShared.HostApp;
using Speckle.Connectors.CSiShared.HostApp.Helpers;
using Speckle.Connectors.ETABSShared.HostApp;
using Speckle.Connectors.ETABSShared.HostApp.Helpers;
using Speckle.Connectors.ETABSShared.HostApp.Services;
using Speckle.Converters.ETABSShared;
namespace Speckle.Connectors.ETABSShared;
@@ -13,12 +12,11 @@ public static class ServiceRegistration
public static IServiceCollection AddEtabs(this IServiceCollection services)
{
services.AddEtabsConverters();
services.AddScoped<CsiSendCollectionManager, EtabsSendCollectionManager>();
services.AddScoped<EtabsSectionPropertyDefinitionService>();
services.AddScoped<EtabsSectionPropertyExtractor>();
services.AddScoped<EtabsShellSectionResolver>();
services.AddScoped<IApplicationFrameSectionPropertyExtractor, EtabsFrameSectionPropertyExtractor>();
services.AddScoped<IApplicationShellSectionPropertyExtractor, EtabsShellSectionPropertyExtractor>();
services.AddScoped<EtabsSectionPropertyExtractor>();
services.AddScoped<EtabsShellSectionResolver>();
services.AddScoped<CsiSendCollectionManager, EtabsSendCollectionManager>();
services.AddScoped<ISectionUnpacker, EtabsSectionUnpacker>();
return services;
@@ -12,7 +12,6 @@
<Compile Include="$(MSBuildThisFileDirectory)HostApp\EtabsSectionUnpacker.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\EtabsSendCollectionManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\EtabsFrameSectionPropertyExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\EtabsSectionPropertyDefinitionService.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\EtabsSectionPropertyExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\EtabsShellSectionPropertyExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Helpers\EtabsShellSectionResolver.cs" />
@@ -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>
@@ -32,7 +32,6 @@ public static class ServiceRegistration
serviceCollection.AddScoped<CsiPierForceResultsExtractor>();
serviceCollection.AddScoped<CsiSpandrelForceResultsExtractor>();
serviceCollection.AddScoped<CsiStoryDriftsResultsExtractor>();
serviceCollection.AddScoped<CsiStoryForceResultsExtractor>();
serviceCollection.AddScoped<ResultsArrayProcessor>();
// Register connector caches
@@ -28,7 +28,6 @@
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\CsiShellPropertiesExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\CsiSpandrelForceResultsExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\CsiStoryDriftsResultsExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\CsiStoryForceResultsExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\CsiToSpeckleCacheSingleton.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\DatabaseTableExtractor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ToSpeckle\Helpers\IApplicationResultsExtractor.cs" />
@@ -1,156 +0,0 @@
using Speckle.Converters.Common;
using Speckle.Converters.CSiShared.Utils;
namespace Speckle.Converters.CSiShared.ToSpeckle.Helpers;
public class CsiStoryForceResultsExtractor : IApplicationResultsExtractor
{
private readonly IConverterSettingsStore<CsiConversionSettings> _settingsStore;
private readonly DatabaseTableExtractor _databaseTableExtractor;
private readonly ResultsArrayProcessor _resultsArrayProcessor;
private const string AXIAL_FORCE = "P";
private const string LOAD_CASE = "LoadCase";
private const string LOCATION = "Location";
private const string MAJOR_MOMENT = "MX";
private const string MAJOR_SHEAR = "VX";
private const string MINOR_MOMENT = "MY";
private const string MINOR_SHEAR = "VY";
private const string OUTPUT_CASE = "OutputCase";
private const string STORY = "Story";
private const string STORY_FORCES = "Story Forces";
private const string TORSION = "T";
public string ResultsKey => "storyForces";
public ModelObjectType TargetObjectType => ModelObjectType.NONE;
public ResultsConfiguration Configuration { get; } =
new([STORY, LOAD_CASE, LOCATION], [AXIAL_FORCE, MAJOR_SHEAR, MINOR_SHEAR, TORSION, MAJOR_MOMENT, MINOR_MOMENT]);
public CsiStoryForceResultsExtractor(
IConverterSettingsStore<CsiConversionSettings> settingsStore,
DatabaseTableExtractor databaseTableExtractor,
ResultsArrayProcessor resultsArrayProcessor
)
{
_settingsStore = settingsStore;
_databaseTableExtractor = databaseTableExtractor;
_resultsArrayProcessor = resultsArrayProcessor;
}
// NOTE: these aren't object specific, they're independent of the user selection, therefore discared
public Dictionary<string, object> GetResults(IEnumerable<string>? objectNames = null)
{
// Step 1: use DatabaseTableExtractor to get results
// NOTE: this differs from other results since Story Forces doesn't have a SapModel.Results.StoryForces method
var tableData = _databaseTableExtractor
.GetTableData(STORY_FORCES, STORY, additionalKeyColumns: [OUTPUT_CASE, LOCATION])
.Rows;
// Get user selected load cases and combinations for filtering
var userSelectedLoadCases = _settingsStore.Current.SelectedLoadCasesAndCombinations?.ToHashSet();
if (userSelectedLoadCases == null)
{
// NOTE: this should never happen as we validate in root object builder
throw new InvalidOperationException("No load cases or combinations selected");
}
// Step 2: Filter out entries that don't match user's selected load cases/combinations
// and organize arrays for dictionary processor
var filteredEntries = tableData
.Where(entry => userSelectedLoadCases.Count == 0 || userSelectedLoadCases.Contains(GetOutputCase(entry.Value)))
.ToList();
if (filteredEntries.Count == 0)
{
throw new InvalidOperationException(
"No load cases or combinations in database match user-selected load cases and combinations"
); // shouldn't fail silently
}
// Step 3: Extract arrays from the nested dictionary structure
var stories = new string[filteredEntries.Count];
var loadCases = new string[filteredEntries.Count];
var locations = new string[filteredEntries.Count];
var pValues = new double[filteredEntries.Count];
var vxValues = new double[filteredEntries.Count];
var vyValues = new double[filteredEntries.Count];
var tValues = new double[filteredEntries.Count];
var mxValues = new double[filteredEntries.Count];
var myValues = new double[filteredEntries.Count];
for (int i = 0; i < filteredEntries.Count; i++)
{
var entry = filteredEntries[i];
var nestedDict = entry.Value;
// Extract Story, OutputCase, and Location directly from the nested dictionary
if (!nestedDict.TryGetValue(STORY, out var story) || string.IsNullOrEmpty(story))
{
throw new InvalidOperationException($"Missing or empty 'Story' column in database row {i}");
}
stories[i] = story;
loadCases[i] = nestedDict.TryGetValue(OUTPUT_CASE, out var loadCase) ? loadCase : string.Empty;
locations[i] = nestedDict.TryGetValue(LOCATION, out var location) ? location : string.Empty;
// Extract force values directly from nested dictionary using field names as keys
pValues[i] = TryParseDouble(nestedDict.TryGetValue(AXIAL_FORCE, out var p) ? p : null);
vxValues[i] = TryParseDouble(nestedDict.TryGetValue(MAJOR_SHEAR, out var vx) ? vx : null);
vyValues[i] = TryParseDouble(nestedDict.TryGetValue(MINOR_SHEAR, out var vy) ? vy : null);
tValues[i] = TryParseDouble(nestedDict.TryGetValue(TORSION, out var t) ? t : null);
mxValues[i] = TryParseDouble(nestedDict.TryGetValue(MAJOR_MOMENT, out var mx) ? mx : null);
myValues[i] = TryParseDouble(nestedDict.TryGetValue(MINOR_MOMENT, out var my) ? my : null);
}
var rawArrays = new Dictionary<string, object>
{
[STORY] = stories,
[LOAD_CASE] = loadCases,
[LOCATION] = locations,
[AXIAL_FORCE] = pValues,
[MAJOR_SHEAR] = vxValues,
[MINOR_SHEAR] = vyValues,
[TORSION] = tValues,
[MAJOR_MOMENT] = mxValues,
[MINOR_MOMENT] = myValues
};
// Step 4: return sorted and processed dictionary
return _resultsArrayProcessor.ProcessArrays(rawArrays, Configuration);
}
/// <summary>
/// Extracts the OutputCase from the nested dictionary structure.
/// This is used for filtering against user selected load cases.
/// </summary>
/// <remarks>
/// All database values are strings
/// </remarks>
private static string GetOutputCase(IReadOnlyDictionary<string, string> nestedDict) =>
nestedDict.TryGetValue(OUTPUT_CASE, out var outputCase) ? outputCase : string.Empty;
/// <summary>
/// Safely parses a value to double, returning 0.0 if parsing fails.
/// Database returns all values as strings, so conversion is needed.
/// </summary>
private static double TryParseDouble(object? value)
{
if (value == null)
{
throw new InvalidOperationException("Cannot parse null value to double in story force results");
}
var stringValue = value.ToString();
if (string.IsNullOrEmpty(stringValue))
{
throw new InvalidOperationException("Cannot parse empty string to double in story force results");
}
if (!double.TryParse(stringValue, out double result))
{
throw new InvalidOperationException($"Failed to parse '{stringValue}' as double in story force results");
}
return result;
}
}
@@ -5,15 +5,15 @@ public class CsiToSpeckleCacheSingleton
/// <summary>
/// A map of (material id, section ids). Assumes the material id is the unique name of the material
/// </summary>
public Dictionary<string, List<string>> MaterialCache { get; set; } = [];
public Dictionary<string, List<string>> MaterialCache { get; set; } = new();
/// <summary>
/// A map of (section id, frame object id). Assumes the section id is the unique name of the section
/// </summary>
public Dictionary<string, List<string>> FrameSectionCache { get; set; } = [];
public Dictionary<string, List<string>> FrameSectionCache { get; set; } = new();
/// <summary>
/// A map of (section id, shell object id). Assumes the section id is the unique name of the section
/// </summary>
public Dictionary<string, List<string>> ShellSectionCache { get; set; } = [];
public Dictionary<string, List<string>> ShellSectionCache { get; set; } = new();
}
@@ -31,10 +31,9 @@ public static class ObjectPropertyKey
public static class SectionPropertyCategory
{
public const string GENERAL_DATA = "General Data";
public const string MODIFIERS = "Modifiers";
public const string PROPERTY_DATA = "Property Data";
public const string SECTION_PROPERTIES = "Section Properties";
public const string SECTION_DIMENSIONS = "Section Dimensions";
public const string PROPERTY_DATA = "Property Data";
}
/// <summary>
@@ -66,7 +65,6 @@ public static class ResultsKey
public const string PIER_FORCES = "Pier Forces";
public const string SPANDREL_FORCES = "Spandrel Forces";
public const string STORY_DRIFTS = "Story Drifts";
public const string STORY_FORCES = "Story Forces";
// Used by ResultTypeSetting to get all defined result keys
public static readonly string[] All =
@@ -77,7 +75,6 @@ public static class ResultsKey
MODAL_PERIOD,
PIER_FORCES,
SPANDREL_FORCES,
STORY_DRIFTS,
STORY_FORCES
STORY_DRIFTS
];
}
@@ -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()
{