Compare commits

..

4 Commits

Author SHA1 Message Date
Jedd Morgan 58c6370cda Merge pull request #1108 from specklesystems/dev
.NET Build and Publish / build-windows (push) Has been cancelled
.NET Build and Publish / build-linux (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
Main to Dev for release
2025-09-24 11:28:14 +01:00
Björn Steinhagen 0e72adba36 fix(etabs): improve frame section property extraction performance using database tables (#1107)
* refactor: using databases for section frame section proxies

* fix: encountered edge cases

* fix: unnecessary usings

* chore: docs
2025-09-24 11:50:20 +02:00
Björn Steinhagen d5084dc334 feat(etabs): add story force results extraction (#1104)
* feat: adds story forces extraction
* fix: const
2025-09-23 15:47:23 +00:00
Jedd Morgan 2c13c4ff79 Merge pull request #1086 from specklesystems/dev
.NET Build and Publish / build-windows (push) Has been cancelled
.NET Build and Publish / build-linux (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
dev -> main
2025-09-10 17:47:31 +01:00
19 changed files with 415 additions and 362 deletions
@@ -20,23 +20,8 @@ public class CsiFrameSectionPropertyExtractor : IFrameSectionPropertyExtractor
_settingsStore = settingsStore;
}
public void ExtractProperties(string sectionName, Dictionary<string, object?> properties)
{
GetMaterialName(sectionName, properties);
public void ExtractProperties(string sectionName, Dictionary<string, object?> 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)
{
@@ -69,47 +54,20 @@ 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.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;
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);
}
}
@@ -23,6 +23,7 @@ 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,18 +47,17 @@ public class EtabsSectionUnpacker : ISectionUnpacker
string sectionName = entry.Key;
List<string> frameIds = entry.Value;
// Initialize properties outside the if statement
Dictionary<string, object?> properties = new Dictionary<string, object?>();
// initialize properties
Dictionary<string, object?> properties = [];
// 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")
// 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))
{
properties = _propertyExtractor.ExtractFrameSectionProperties(sectionName);
}
// create the section proxy
// create section proxy
GroupProxy sectionProxy =
new()
{
@@ -66,8 +65,8 @@ public class EtabsSectionUnpacker : ISectionUnpacker
name = sectionName,
applicationId = sectionName,
objects = frameIds,
["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
["type"] = "Frame Section",
["properties"] = properties
};
yield return sectionProxy;
@@ -81,8 +80,8 @@ public class EtabsSectionUnpacker : ISectionUnpacker
string sectionName = entry.Key;
List<string> frameIds = entry.Value;
// Initialize properties outside the if statement
Dictionary<string, object?> properties = new Dictionary<string, object?>();
// initialize properties outside the if statement
Dictionary<string, object?> properties = [];
// get the properties of the section
// openings will have objects assigned to them, but won't have properties
@@ -92,7 +91,7 @@ public class EtabsSectionUnpacker : ISectionUnpacker
properties = _propertyExtractor.ExtractShellSectionProperties(sectionName);
}
// create the section proxy
// create section proxy
GroupProxy sectionProxy =
new()
{
@@ -1,4 +1,5 @@
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;
@@ -8,69 +9,96 @@ 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)
public EtabsFrameSectionPropertyExtractor(
IConverterSettingsStore<CsiConversionSettings> settingsStore,
EtabsSectionPropertyDefinitionService definitionService
)
{
_settingsStore = settingsStore;
_definitionService = definitionService;
}
/// <summary>
/// Gets generalised frame section properties
/// Gets frame section properties from preloaded database table data
/// </summary>
/// <remarks>
/// 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.
/// Property categorization is done heuristically - order matters in the parsing logic.
/// </remarks>
public void ExtractProperties(string sectionName, Dictionary<string, object?> properties)
{
// 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)
// 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))
{
// General Data
var generalData = properties.EnsureNested(SectionPropertyCategory.GENERAL_DATA);
generalData["Section Shape"] = propTypes[sectionIndex].ToString();
return; // no definitions found for this section
}
// 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}²");
// 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
}
}
}
}
@@ -0,0 +1,105 @@
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 = new();
Dictionary<string, object?> properties = [];
_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 = new();
Dictionary<string, object?> properties = [];
_csiShellExtractor.ExtractProperties(sectionName, properties);
_etabsShellExtractor.ExtractProperties(sectionName, properties);
return properties;
@@ -3,6 +3,7 @@ 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;
@@ -12,11 +13,12 @@ public static class ServiceRegistration
public static IServiceCollection AddEtabs(this IServiceCollection services)
{
services.AddEtabsConverters();
services.AddScoped<IApplicationFrameSectionPropertyExtractor, EtabsFrameSectionPropertyExtractor>();
services.AddScoped<IApplicationShellSectionPropertyExtractor, EtabsShellSectionPropertyExtractor>();
services.AddScoped<CsiSendCollectionManager, EtabsSendCollectionManager>();
services.AddScoped<EtabsSectionPropertyDefinitionService>();
services.AddScoped<EtabsSectionPropertyExtractor>();
services.AddScoped<EtabsShellSectionResolver>();
services.AddScoped<CsiSendCollectionManager, EtabsSendCollectionManager>();
services.AddScoped<IApplicationFrameSectionPropertyExtractor, EtabsFrameSectionPropertyExtractor>();
services.AddScoped<IApplicationShellSectionPropertyExtractor, EtabsShellSectionPropertyExtractor>();
services.AddScoped<ISectionUnpacker, EtabsSectionUnpacker>();
return services;
@@ -12,6 +12,7 @@
<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,49 +112,10 @@ 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));
@@ -330,84 +291,6 @@ 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,6 +32,7 @@ public static class ServiceRegistration
serviceCollection.AddScoped<CsiPierForceResultsExtractor>();
serviceCollection.AddScoped<CsiSpandrelForceResultsExtractor>();
serviceCollection.AddScoped<CsiStoryDriftsResultsExtractor>();
serviceCollection.AddScoped<CsiStoryForceResultsExtractor>();
serviceCollection.AddScoped<ResultsArrayProcessor>();
// Register connector caches
@@ -28,6 +28,7 @@
<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" />
@@ -0,0 +1,156 @@
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; } = new();
public Dictionary<string, List<string>> MaterialCache { get; set; } = [];
/// <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; } = new();
public Dictionary<string, List<string>> FrameSectionCache { get; set; } = [];
/// <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; } = new();
public Dictionary<string, List<string>> ShellSectionCache { get; set; } = [];
}
@@ -31,9 +31,10 @@ 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>
@@ -65,6 +66,7 @@ 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 =
@@ -75,6 +77,7 @@ public static class ResultsKey
MODAL_PERIOD,
PIER_FORCES,
SPANDREL_FORCES,
STORY_DRIFTS
STORY_DRIFTS,
STORY_FORCES
];
}
@@ -33,9 +33,6 @@ public class DisplayValueExtractor
return [];
}
// Check if this is an instance definition by looking at InstanceHashCode
bool isInstanceDefinition = modelItem.InstanceHashCode != 0;
return !IsElementVisible(modelItem) ? [] : _geometryConverter.Convert(modelItem, isInstanceDefinition);
return !IsElementVisible(modelItem) ? [] : _geometryConverter.Convert(modelItem);
}
}
@@ -1,44 +0,0 @@
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,5 +50,4 @@ 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, bool isInstanceDefinition = false)
internal List<Base> Convert(NAV.ModelItem modelItem)
{
if (modelItem == null)
{
@@ -64,7 +64,7 @@ public class GeometryToSpeckleConverter
CollectFragments(path, fragmentStack);
}
return ProcessFragments(fragmentStack, paths, isInstanceDefinition);
return ProcessFragments(fragmentStack, paths);
}
finally
{
@@ -103,7 +103,7 @@ public class GeometryToSpeckleConverter
}
}
private List<Base> ProcessFragments(Stack<InwOaFragment3> fragmentStack, InwSelectionPathsColl paths, bool isInstanceDefinition = false)
private List<Base> ProcessFragments(Stack<InwOaFragment3> fragmentStack, InwSelectionPathsColl paths)
{
var callbackListeners = new List<PrimitiveProcessor>();
@@ -132,7 +132,7 @@ public class GeometryToSpeckleConverter
callbackListeners.Add(processor);
}
var baseGeometries = ProcessGeometries(callbackListeners, isInstanceDefinition);
var baseGeometries = ProcessGeometries(callbackListeners);
return baseGeometries;
}
@@ -147,7 +147,7 @@ public class GeometryToSpeckleConverter
return IsSameFragmentPath(fragmentPathData, pathData);
}
private List<Base> ProcessGeometries(List<PrimitiveProcessor> processors, bool isInstanceDefinition = false)
private List<Base> ProcessGeometries(List<PrimitiveProcessor> processors)
{
var baseGeometries = new List<Base>();
@@ -155,13 +155,13 @@ public class GeometryToSpeckleConverter
{
if (processor.Triangles.Count > 0)
{
var mesh = CreateMesh(processor.Triangles, isInstanceDefinition);
var mesh = CreateMesh(processor.Triangles);
baseGeometries.Add(mesh);
}
if (processor.Lines.Count > 0)
{
var lines = CreateLines(processor.Lines, isInstanceDefinition);
var lines = CreateLines(processor.Lines);
baseGeometries.AddRange(lines);
}
}
@@ -169,7 +169,7 @@ public class GeometryToSpeckleConverter
return baseGeometries;
}
private Mesh CreateMesh(IReadOnlyList<SafeTriangle> triangles, bool isInstanceDefinition = false)
private Mesh CreateMesh(IReadOnlyList<SafeTriangle> triangles)
{
var vertices = new List<double>();
var faces = new List<int>();
@@ -178,40 +178,20 @@ public class GeometryToSpeckleConverter
{
var triangle = triangles[t];
// 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
]
);
}
// 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
]
);
faces.AddRange([3, t * 3, t * 3 + 1, t * 3 + 2]);
}
@@ -223,37 +203,23 @@ public class GeometryToSpeckleConverter
};
}
private List<Line> CreateLines(IReadOnlyList<SafeLine> lines, bool isInstanceDefinition = false) =>
private List<Line> CreateLines(IReadOnlyList<SafeLine> lines) =>
(
from line in lines
select new Line
{
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
),
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
),
units = _settings.Derived.SpeckleUnits
}
).ToList();
@@ -40,7 +40,6 @@ 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)
{
@@ -52,7 +51,6 @@ 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()
@@ -63,7 +61,6 @@ public class ModelItemToToSpeckleConverter : IToSpeckleTopLevelConverter
units = _settingsStore.Current.Derived.SpeckleUnits,
};
private Collection CreateNonGeometryObject(NAV.ModelItem target, string name, IPropertyHandler propertyHandler) =>
new()
{