diff --git a/Connectors/CSi/Speckle.Connectors.CSiShared/Bindings/CsiSharedSelectionBinding.cs b/Connectors/CSi/Speckle.Connectors.CSiShared/Bindings/CsiSharedSelectionBinding.cs index 7981a3029..b67df4f5d 100644 --- a/Connectors/CSi/Speckle.Connectors.CSiShared/Bindings/CsiSharedSelectionBinding.cs +++ b/Connectors/CSi/Speckle.Connectors.CSiShared/Bindings/CsiSharedSelectionBinding.cs @@ -2,6 +2,7 @@ using Speckle.Connectors.CSiShared.Utils; using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; +using Speckle.Converters.CSiShared.Utils; namespace Speckle.Connectors.CSiShared.Bindings; @@ -25,18 +26,6 @@ public CsiSharedSelectionBinding(IBrowserBridge parent, ICsiApplicationService c /// public SelectionInfo GetSelection() { - // TODO: Since this is standard across CSi Suite - better stored in an enum? - var objectTypeMap = new Dictionary - { - { 1, "Point" }, - { 2, "Frame" }, - { 3, "Cable" }, - { 4, "Tendon" }, - { 5, "Area" }, - { 6, "Solid" }, - { 7, "Link" } - }; - int numberItems = 0; int[] objectType = Array.Empty(); string[] objectName = Array.Empty(); @@ -48,10 +37,10 @@ public SelectionInfo GetSelection() for (int i = 0; i < numberItems; i++) { - var typeKey = objectType[i]; - var typeName = objectTypeMap.TryGetValue(typeKey, out var name) ? name : $"Unknown ({typeKey})"; + var typeKey = (ModelObjectType)objectType[i]; + var typeName = typeKey.ToString(); - encodedIds.Add(ObjectIdentifier.Encode(typeKey, objectName[i])); + encodedIds.Add(ObjectIdentifier.Encode(objectType[i], objectName[i])); typeCounts[typeName] = (typeCounts.TryGetValue(typeName, out var count) ? count : 0) + 1; // NOTE: Cross-framework compatibility (net 48 and net8) } diff --git a/Connectors/CSi/Speckle.Connectors.CSiShared/HostApp/Helpers/CsiFrameSectionPropertyExtractor.cs b/Connectors/CSi/Speckle.Connectors.CSiShared/HostApp/Helpers/CsiFrameSectionPropertyExtractor.cs new file mode 100644 index 000000000..72d82a78a --- /dev/null +++ b/Connectors/CSi/Speckle.Connectors.CSiShared/HostApp/Helpers/CsiFrameSectionPropertyExtractor.cs @@ -0,0 +1,117 @@ +using Speckle.Converters.Common; +using Speckle.Converters.CSiShared; +using Speckle.Converters.CSiShared.Utils; + +namespace Speckle.Connectors.CSiShared.HostApp.Helpers; + +/// +/// Base frame section property extractor for CSi products. +/// +/// +/// Handles common Csi API calls for frame section properties +/// Provides foundation for application-specific extractors. +/// +public class CsiFrameSectionPropertyExtractor : IFrameSectionPropertyExtractor +{ + private readonly IConverterSettingsStore _settingsStore; + + public CsiFrameSectionPropertyExtractor(IConverterSettingsStore settingsStore) + { + _settingsStore = settingsStore; + } + + public void ExtractProperties(string sectionName, Dictionary properties) + { + GetMaterialName(sectionName, properties); + GetSectionProperties(sectionName, properties); + GetPropertyModifiers(sectionName, properties); + } + + private void GetMaterialName(string sectionName, Dictionary properties) + { + // get material name + string materialName = string.Empty; + _settingsStore.Current.SapModel.PropFrame.GetMaterial(sectionName, ref materialName); + + // append to General Data of properties dictionary + Dictionary generalData = DictionaryUtils.EnsureNestedDictionary( + properties, + SectionPropertyCategory.GENERAL_DATA + ); + generalData["material"] = materialName; + } + + private void GetSectionProperties(string sectionName, Dictionary properties) + { + double crossSectionalArea = 0, + shearAreaInMajorAxisDirection = 0, + shearAreaInMinorAxisDirection = 0, + torsionalConstant = 0, + momentOfInertiaAboutMajorAxis = 0, + momentOfInertiaAboutMinorAxis = 0, + sectionModulusAboutMajorAxis = 0, + sectionModulusAboutMinorAxis = 0, + plasticModulusAboutMajorAxis = 0, + plasticModulusAboutMinorAxis = 0, + radiusOfGyrationAboutMajorAxis = 0, + radiusOfGyrationAboutMinorAxis = 0; + + _settingsStore.Current.SapModel.PropFrame.GetSectProps( + sectionName, + ref crossSectionalArea, + ref shearAreaInMajorAxisDirection, + ref shearAreaInMinorAxisDirection, + ref torsionalConstant, + ref momentOfInertiaAboutMajorAxis, + ref momentOfInertiaAboutMinorAxis, + ref sectionModulusAboutMajorAxis, + ref sectionModulusAboutMinorAxis, + ref plasticModulusAboutMajorAxis, + ref plasticModulusAboutMinorAxis, + ref radiusOfGyrationAboutMajorAxis, + ref radiusOfGyrationAboutMinorAxis + ); + + Dictionary mechanicalProperties = DictionaryUtils.EnsureNestedDictionary( + properties, + SectionPropertyCategory.SECTION_PROPERTIES + ); + mechanicalProperties["area"] = crossSectionalArea; + mechanicalProperties["As2"] = shearAreaInMajorAxisDirection; + mechanicalProperties["As3"] = shearAreaInMinorAxisDirection; + mechanicalProperties["torsion"] = torsionalConstant; + mechanicalProperties["I22"] = momentOfInertiaAboutMajorAxis; + mechanicalProperties["I33"] = momentOfInertiaAboutMinorAxis; + mechanicalProperties["S22"] = sectionModulusAboutMajorAxis; + mechanicalProperties["S33"] = sectionModulusAboutMinorAxis; + mechanicalProperties["Z22"] = plasticModulusAboutMajorAxis; + mechanicalProperties["Z33"] = plasticModulusAboutMinorAxis; + mechanicalProperties["R22"] = radiusOfGyrationAboutMajorAxis; + mechanicalProperties["R33"] = radiusOfGyrationAboutMinorAxis; + } + + private void GetPropertyModifiers(string sectionName, Dictionary properties) + { + double[] stiffnessModifiersArray = []; + _settingsStore.Current.SapModel.PropFrame.GetModifiers(sectionName, ref stiffnessModifiersArray); + + Dictionary modifiers = + new() + { + ["crossSectionalAreaModifier"] = stiffnessModifiersArray[0], + ["shearAreaInLocal2DirectionModifier"] = stiffnessModifiersArray[1], + ["shearAreaInLocal3DirectionModifier"] = stiffnessModifiersArray[2], + ["torsionalConstantModifier"] = stiffnessModifiersArray[3], + ["momentOfInertiaAboutLocal2AxisModifier"] = stiffnessModifiersArray[4], + ["momentOfInertiaAboutLocal3AxisModifier"] = stiffnessModifiersArray[5], + ["mass"] = stiffnessModifiersArray[6], + ["weight"] = stiffnessModifiersArray[7], + }; + + Dictionary generalData = DictionaryUtils.EnsureNestedDictionary( + properties, + SectionPropertyCategory.GENERAL_DATA + ); + generalData["modifiers"] = modifiers; + } +} diff --git a/Connectors/CSi/Speckle.Connectors.CSiShared/HostApp/Helpers/CsiMaterialPropertyExtractor.cs b/Connectors/CSi/Speckle.Connectors.CSiShared/HostApp/Helpers/CsiMaterialPropertyExtractor.cs new file mode 100644 index 000000000..3dcaa8d1a --- /dev/null +++ b/Connectors/CSi/Speckle.Connectors.CSiShared/HostApp/Helpers/CsiMaterialPropertyExtractor.cs @@ -0,0 +1,212 @@ +using Speckle.Converters.Common; +using Speckle.Converters.CSiShared; +using Speckle.Converters.CSiShared.Utils; + +namespace Speckle.Connectors.CSiShared.HostApp.Helpers; + +/// +/// Base material property extractor for CSi products. +/// +/// +/// Currently, all material property extraction can happen on a CsiShared level which simplifies things a lot. +/// Properties depend on the directional symmetry of the material, hence the switch statements. +/// +public class CsiMaterialPropertyExtractor +{ + /// + /// Property strings for all mechanical properties, used by numerous methods. + /// + private static class MechanicalPropertyNames + { + public const string MODULUS_OF_ELASTICITY = "modulusOfElasticity"; + public const string MODULUS_OF_ELASTICITY_ARRAY = "modulusOfElasticityArray"; + public const string POISSON_RATIO = "poissonRatio"; + public const string POISSON_RATIO_ARRAY = "poissonRatioArray"; + public const string THERMAL_COEFFICIENT = "thermalCoefficient"; + public const string THERMAL_COEFFICIENT_ARRAY = "thermalCoefficientArray"; + public const string SHEAR_MODULUS = "shearModulus"; + public const string SHEAR_MODULUS_ARRAY = "shearModulusArray"; + } + + private readonly IConverterSettingsStore _settingsStore; + + public CsiMaterialPropertyExtractor(IConverterSettingsStore settingsStore) + { + _settingsStore = settingsStore; + } + + public void ExtractProperties(string materialName, Dictionary properties) + { + GetGeneralProperties(materialName, properties); + GetWeightAndMassProperties(materialName, properties); + GetMechanicalProperties(materialName, properties); + } + + private void GetGeneralProperties(string materialName, Dictionary properties) + { + { + eMatType materialType = default; + int materialColor = 0; + string materialNotes = string.Empty; + string materialGuid = string.Empty; + + _settingsStore.Current.SapModel.PropMaterial.GetMaterial( + materialName, + ref materialType, + ref materialColor, + ref materialNotes, + ref materialGuid + ); + + var generalData = DictionaryUtils.EnsureNestedDictionary(properties, SectionPropertyCategory.GENERAL_DATA); + generalData["name"] = materialName; + generalData["type"] = materialType.ToString(); + generalData["notes"] = materialNotes; + } + } + + private void GetWeightAndMassProperties(string materialName, Dictionary properties) + { + double weightPerUnitVolume = double.NaN; + double massPerUnitVolume = double.NaN; + + _settingsStore.Current.SapModel.PropMaterial.GetWeightAndMass( + materialName, + ref weightPerUnitVolume, + ref massPerUnitVolume + ); + + var weightAndMass = DictionaryUtils.EnsureNestedDictionary(properties, "Weight and Mass"); + weightAndMass["w"] = weightPerUnitVolume; + weightAndMass["m"] = massPerUnitVolume; + } + + private void GetMechanicalProperties(string materialName, Dictionary properties) + { + int materialDirectionalSymmetryKey = 0; + eMatType materialType = default; + + _settingsStore.Current.SapModel.PropMaterial.GetTypeOAPI( + materialName, + ref materialType, + ref materialDirectionalSymmetryKey + ); + + var materialDirectionalSymmetryValue = materialDirectionalSymmetryKey switch + { + 0 => DirectionalSymmetryType.ISOTROPIC, + 1 => DirectionalSymmetryType.ORTHOTROPIC, + 2 => DirectionalSymmetryType.ANISOTROPIC, + 3 => DirectionalSymmetryType.UNIAXIAL, + _ => throw new ArgumentException($"Unknown symmetry type: {materialDirectionalSymmetryKey}") + }; + + var mechanicalProperties = DictionaryUtils.EnsureNestedDictionary(properties, "Mechanical Properties"); + mechanicalProperties["directionalSymmetryType"] = materialDirectionalSymmetryValue.ToString(); + + GetMechanicalPropertiesByType(materialName, materialDirectionalSymmetryValue, mechanicalProperties); + } + + private void GetMechanicalPropertiesByType( + string materialName, + DirectionalSymmetryType directionalSymmetryType, + Dictionary mechanicalProperties + ) + { + switch (directionalSymmetryType) + { + case DirectionalSymmetryType.ISOTROPIC: + ExtractIsotropicProperties(materialName, mechanicalProperties); + break; + case DirectionalSymmetryType.ORTHOTROPIC: + ExtractOrthotropicProperties(materialName, mechanicalProperties); + break; + case DirectionalSymmetryType.ANISOTROPIC: + ExtractAnisotropicProperties(materialName, mechanicalProperties); + break; + case DirectionalSymmetryType.UNIAXIAL: + ExtractUniaxialProperties(materialName, mechanicalProperties); + break; + default: + throw new ArgumentException($"Unknown directional symmetry type: {directionalSymmetryType}"); + } + } + + private void ExtractIsotropicProperties(string materialName, Dictionary mechanicalProperties) + { + double modulusOfElasticity = double.NaN; + double poissonRatio = double.NaN; + double thermalCoefficient = double.NaN; + double shearModulus = double.NaN; + + _settingsStore.Current.SapModel.PropMaterial.GetMPIsotropic( + materialName, + ref modulusOfElasticity, + ref poissonRatio, + ref thermalCoefficient, + ref shearModulus + ); + + mechanicalProperties[MechanicalPropertyNames.MODULUS_OF_ELASTICITY] = modulusOfElasticity; + mechanicalProperties[MechanicalPropertyNames.POISSON_RATIO] = poissonRatio; + mechanicalProperties[MechanicalPropertyNames.THERMAL_COEFFICIENT] = thermalCoefficient; + mechanicalProperties[MechanicalPropertyNames.SHEAR_MODULUS] = shearModulus; + } + + private void ExtractOrthotropicProperties(string materialName, Dictionary mechanicalProperties) + { + double[] modulusOfElasticityArray = []; + double[] poissonRatioArray = []; + double[] thermalCoefficientArray = []; + double[] shearModulusArray = []; + + _settingsStore.Current.SapModel.PropMaterial.GetMPOrthotropic( + materialName, + ref modulusOfElasticityArray, + ref poissonRatioArray, + ref thermalCoefficientArray, + ref shearModulusArray + ); + + mechanicalProperties[MechanicalPropertyNames.MODULUS_OF_ELASTICITY_ARRAY] = modulusOfElasticityArray; + mechanicalProperties[MechanicalPropertyNames.POISSON_RATIO_ARRAY] = poissonRatioArray; + mechanicalProperties[MechanicalPropertyNames.THERMAL_COEFFICIENT_ARRAY] = thermalCoefficientArray; + mechanicalProperties[MechanicalPropertyNames.SHEAR_MODULUS_ARRAY] = shearModulusArray; + } + + private void ExtractAnisotropicProperties(string materialName, Dictionary mechanicalProperties) + { + double[] modulusOfElasticityArray = []; + double[] poissonRatioArray = []; + double[] thermalCoefficientArray = []; + double[] shearModulusArray = []; + + _settingsStore.Current.SapModel.PropMaterial.GetMPAnisotropic( + materialName, + ref modulusOfElasticityArray, + ref poissonRatioArray, + ref thermalCoefficientArray, + ref shearModulusArray + ); + + mechanicalProperties[MechanicalPropertyNames.MODULUS_OF_ELASTICITY_ARRAY] = modulusOfElasticityArray; + mechanicalProperties[MechanicalPropertyNames.POISSON_RATIO_ARRAY] = poissonRatioArray; + mechanicalProperties[MechanicalPropertyNames.THERMAL_COEFFICIENT_ARRAY] = thermalCoefficientArray; + mechanicalProperties[MechanicalPropertyNames.SHEAR_MODULUS_ARRAY] = shearModulusArray; + } + + private void ExtractUniaxialProperties(string materialName, Dictionary mechanicalProperties) + { + double modulusOfElasticity = double.NaN; + double thermalCoefficient = double.NaN; + + _settingsStore.Current.SapModel.PropMaterial.GetMPUniaxial( + materialName, + ref modulusOfElasticity, + ref thermalCoefficient + ); + + mechanicalProperties[MechanicalPropertyNames.MODULUS_OF_ELASTICITY] = modulusOfElasticity; + mechanicalProperties[MechanicalPropertyNames.THERMAL_COEFFICIENT] = thermalCoefficient; + } +} diff --git a/Connectors/CSi/Speckle.Connectors.CSiShared/HostApp/Helpers/CsiShellSectionPropertyExtractor.cs b/Connectors/CSi/Speckle.Connectors.CSiShared/HostApp/Helpers/CsiShellSectionPropertyExtractor.cs new file mode 100644 index 000000000..697cdb9b0 --- /dev/null +++ b/Connectors/CSi/Speckle.Connectors.CSiShared/HostApp/Helpers/CsiShellSectionPropertyExtractor.cs @@ -0,0 +1,68 @@ +using Speckle.Converters.Common; +using Speckle.Converters.CSiShared; +using Speckle.Converters.CSiShared.Utils; + +namespace Speckle.Connectors.CSiShared.HostApp.Helpers; + +/// +/// Base shell section property extractor for CSi products. +/// +/// +/// Handles common Csi API calls for shell section properties. +/// Provides foundation for application-specific extractors. +/// +public class CsiShellSectionPropertyExtractor : IShellSectionPropertyExtractor +{ + private readonly IConverterSettingsStore _settingsStore; + + public CsiShellSectionPropertyExtractor(IConverterSettingsStore settingsStore) + { + _settingsStore = settingsStore; + } + + public void ExtractProperties(string sectionName, Dictionary properties) + { + GetPropertyType(sectionName, properties); + GetPropertyModifiers(sectionName, properties); + } + + private void GetPropertyType(string sectionName, Dictionary properties) + { + int propertyTypeKey = 1; + _settingsStore.Current.SapModel.PropArea.GetTypeOAPI(sectionName, ref propertyTypeKey); + var propertyTypeValue = propertyTypeKey switch + { + 1 => AreaPropertyType.SHELL, + 2 => AreaPropertyType.PLANE, + 3 => AreaPropertyType.ASOLID, + _ => throw new ArgumentException($"Unknown property type: {propertyTypeKey}"), + }; + + var generalData = DictionaryUtils.EnsureNestedDictionary(properties, SectionPropertyCategory.GENERAL_DATA); + generalData["propertyType"] = propertyTypeValue; + } + + private void GetPropertyModifiers(string sectionName, Dictionary properties) + { + double[] stiffnessModifiersArray = []; + _settingsStore.Current.SapModel.PropArea.GetModifiers(sectionName, ref stiffnessModifiersArray); + + Dictionary modifiers = + new() + { + ["f11"] = stiffnessModifiersArray[0], + ["f22"] = stiffnessModifiersArray[1], + ["f12"] = stiffnessModifiersArray[2], + ["m11"] = stiffnessModifiersArray[3], + ["m22"] = stiffnessModifiersArray[3], + ["m12"] = stiffnessModifiersArray[4], + ["v13"] = stiffnessModifiersArray[5], + ["v23"] = stiffnessModifiersArray[6], + ["mass"] = stiffnessModifiersArray[7], + ["weight"] = stiffnessModifiersArray[8] + }; + + var generalData = DictionaryUtils.EnsureNestedDictionary(properties, SectionPropertyCategory.GENERAL_DATA); + generalData["modifiers"] = modifiers; + } +} diff --git a/Connectors/CSi/Speckle.Connectors.CSiShared/HostApp/Helpers/IApplicationSectionPropertyExtractor.cs b/Connectors/CSi/Speckle.Connectors.CSiShared/HostApp/Helpers/IApplicationSectionPropertyExtractor.cs new file mode 100644 index 000000000..e16d3b3ea --- /dev/null +++ b/Connectors/CSi/Speckle.Connectors.CSiShared/HostApp/Helpers/IApplicationSectionPropertyExtractor.cs @@ -0,0 +1,18 @@ +namespace Speckle.Connectors.CSiShared.HostApp.Helpers; + +/// +/// Contract for host application specific section property extraction. +/// +/// +/// Mirrors property extraction system pattern by composing with base extractor. +/// Enables both shared and application-specific property extraction in one call. +/// +public interface IApplicationSectionPropertyExtractor +{ + void ExtractProperties(string sectionName, Dictionary properties); +} + +// NOTE: Seemingly silly, but allows us to register the correct extractor for the correct type. +public interface IApplicationFrameSectionPropertyExtractor : IApplicationSectionPropertyExtractor { } + +public interface IApplicationShellSectionPropertyExtractor : IApplicationSectionPropertyExtractor { } diff --git a/Connectors/CSi/Speckle.Connectors.CSiShared/HostApp/Helpers/ISectionPropertyExtractor.cs b/Connectors/CSi/Speckle.Connectors.CSiShared/HostApp/Helpers/ISectionPropertyExtractor.cs new file mode 100644 index 000000000..5f401cdd1 --- /dev/null +++ b/Connectors/CSi/Speckle.Connectors.CSiShared/HostApp/Helpers/ISectionPropertyExtractor.cs @@ -0,0 +1,14 @@ +namespace Speckle.Connectors.CSiShared.HostApp.Helpers; + +/// +/// Core contract for section property extraction common across CSi products. +/// +public interface ISectionPropertyExtractor +{ + void ExtractProperties(string sectionName, Dictionary properties); +} + +// NOTE: Seemingly silly, but allows us to register the correct extractor for the correct type. +public interface IFrameSectionPropertyExtractor : ISectionPropertyExtractor { } + +public interface IShellSectionPropertyExtractor : ISectionPropertyExtractor { } diff --git a/Connectors/CSi/Speckle.Connectors.CSiShared/HostApp/Helpers/ISectionUnpacker.cs b/Connectors/CSi/Speckle.Connectors.CSiShared/HostApp/Helpers/ISectionUnpacker.cs new file mode 100644 index 000000000..cee5215b5 --- /dev/null +++ b/Connectors/CSi/Speckle.Connectors.CSiShared/HostApp/Helpers/ISectionUnpacker.cs @@ -0,0 +1,10 @@ +using Speckle.Sdk.Models.Proxies; + +namespace Speckle.Connectors.CSiShared.HostApp.Helpers; + +// NOTE: Interface because Etabs and Sap2000 section unpacking and extraction is different. +// At ServiceRegistration, we inject the correct implementation of the ISectionUnpacker +public interface ISectionUnpacker +{ + IEnumerable UnpackSections(); +} diff --git a/Connectors/CSi/Speckle.Connectors.CSiShared/HostApp/MaterialUnpacker.cs b/Connectors/CSi/Speckle.Connectors.CSiShared/HostApp/MaterialUnpacker.cs new file mode 100644 index 000000000..1de645a85 --- /dev/null +++ b/Connectors/CSi/Speckle.Connectors.CSiShared/HostApp/MaterialUnpacker.cs @@ -0,0 +1,51 @@ +using Speckle.Connectors.CSiShared.HostApp.Helpers; +using Speckle.Converters.CSiShared.ToSpeckle.Helpers; +using Speckle.Sdk.Models.Proxies; + +namespace Speckle.Connectors.CSiShared.HostApp; + +/// +/// Creates material proxies based on stored entries from the materials cache +/// +public class MaterialUnpacker +{ + private readonly CsiMaterialPropertyExtractor _propertyExtractor; + private readonly CsiToSpeckleCacheSingleton _csiToSpeckleCacheSingleton; + + public MaterialUnpacker( + CsiMaterialPropertyExtractor propertyExtractor, + CsiToSpeckleCacheSingleton csiToSpeckleCacheSingleton + ) + { + _propertyExtractor = propertyExtractor; + _csiToSpeckleCacheSingleton = csiToSpeckleCacheSingleton; + } + + // Creates a list of material proxies from the csi materials cache + public IEnumerable UnpackMaterials() + { + foreach (var kvp in _csiToSpeckleCacheSingleton.MaterialCache) + { + // get the cached entry + string materialName = kvp.Key; + List sectionIds = kvp.Value; + + // get the properties of the material + Dictionary properties = new(); // create empty dictionary + _propertyExtractor.ExtractProperties(materialName, properties); // dictionary mutated with respective properties + + // create the material proxy + GroupProxy materialProxy = + new() + { + id = materialName, + name = materialName, + applicationId = materialName, + objects = sectionIds, + ["properties"] = properties + }; + + yield return materialProxy; + } + } +} diff --git a/Connectors/CSi/Speckle.Connectors.CSiShared/Operations/Send/CsiRootObjectBuilder.cs b/Connectors/CSi/Speckle.Connectors.CSiShared/Operations/Send/CsiRootObjectBuilder.cs index c76aa8dfd..6deb3b5a6 100644 --- a/Connectors/CSi/Speckle.Connectors.CSiShared/Operations/Send/CsiRootObjectBuilder.cs +++ b/Connectors/CSi/Speckle.Connectors.CSiShared/Operations/Send/CsiRootObjectBuilder.cs @@ -4,6 +4,7 @@ using Speckle.Connectors.Common.Conversion; using Speckle.Connectors.Common.Operations; using Speckle.Connectors.CSiShared.HostApp; +using Speckle.Connectors.CSiShared.HostApp.Helpers; using Speckle.Converters.Common; using Speckle.Converters.CSiShared; using Speckle.Sdk; @@ -13,12 +14,27 @@ namespace Speckle.Connectors.CSiShared.Builders; +/// +/// Manages the conversion of CSi model objects and establishes proxy-based relationships. +/// +/// +/// Core responsibilities: +/// - Converts ICsiWrappers to Speckle objects through caching-aware conversion +/// - Creates proxy objects for materials and sections from model data +/// - Establishes relationships between objects and their dependencies +/// +/// The builder follows a two-phase process: +/// 1. Conversion Phase: ICsiWrappers → Speckle objects with cached results handling +/// 2. Relationship Phase: Material/section proxy creation and relationship mapping +/// public class CsiRootObjectBuilder : IRootObjectBuilder { private readonly IRootToSpeckleConverter _rootToSpeckleConverter; private readonly ISendConversionCache _sendConversionCache; private readonly IConverterSettingsStore _converterSettings; private readonly CsiSendCollectionManager _sendCollectionManager; + private readonly MaterialUnpacker _materialUnpacker; + private readonly ISectionUnpacker _sectionUnpacker; private readonly ILogger _logger; private readonly ISdkActivityFactory _activityFactory; private readonly ICsiApplicationService _csiApplicationService; @@ -28,6 +44,8 @@ public CsiRootObjectBuilder( ISendConversionCache sendConversionCache, IConverterSettingsStore converterSettings, CsiSendCollectionManager sendCollectionManager, + MaterialUnpacker materialUnpacker, + ISectionUnpacker sectionUnpacker, ILogger logger, ISdkActivityFactory activityFactory, ICsiApplicationService csiApplicationService @@ -36,12 +54,23 @@ ICsiApplicationService csiApplicationService _sendConversionCache = sendConversionCache; _converterSettings = converterSettings; _sendCollectionManager = sendCollectionManager; + _materialUnpacker = materialUnpacker; + _sectionUnpacker = sectionUnpacker; _rootToSpeckleConverter = rootToSpeckleConverter; _logger = logger; _activityFactory = activityFactory; _csiApplicationService = csiApplicationService; } + /// + /// Converts Csi objects into a Speckle-compatible object hierarchy with established relationships. + /// + /// + /// Operation sequence: + /// 1. Creates root collection with model metadata + /// 2. Converts each object with caching and progress tracking + /// 3. Creates proxies for materials and sections + /// public async Task BuildAsync( IReadOnlyList csiObjects, SendInfo sendInfo, @@ -52,8 +81,8 @@ CancellationToken cancellationToken using var activity = _activityFactory.Start("Build"); string modelFileName = _csiApplicationService.SapModel.GetModelFilename(false) ?? "Unnamed model"; - Collection rootObjectCollection = new() { name = modelFileName }; - rootObjectCollection["units"] = _converterSettings.Current.SpeckleUnits; + Collection rootObjectCollection = + new() { name = modelFileName, ["units"] = _converterSettings.Current.SpeckleUnits }; List results = new(csiObjects.Count); int count = 0; @@ -65,7 +94,7 @@ CancellationToken cancellationToken cancellationToken.ThrowIfCancellationRequested(); using var _2 = _activityFactory.Start("Convert"); - var result = ConvertCSiObject(csiObject, rootObjectCollection, sendInfo.ProjectId); + var result = ConvertCsiObject(csiObject, rootObjectCollection, sendInfo.ProjectId); results.Add(result); count++; @@ -79,10 +108,22 @@ CancellationToken cancellationToken throw new SpeckleException("Failed to convert all objects."); } + using (var _ = _activityFactory.Start("Process Proxies")) + { + // Create and add material proxies + rootObjectCollection[ProxyKeys.MATERIAL] = _materialUnpacker.UnpackMaterials().ToList(); + + // Create and all section proxies (frame and shell) + rootObjectCollection[ProxyKeys.SECTION] = _sectionUnpacker.UnpackSections().ToList(); + } + return new RootObjectBuilderResult(rootObjectCollection, results); } - private SendConversionResult ConvertCSiObject(ICsiWrapper csiObject, Collection typeCollection, string projectId) + /// + /// Converts a single Csi wrapper "object" to a data object with appropriate collection management. + /// + private SendConversionResult ConvertCsiObject(ICsiWrapper csiObject, Collection typeCollection, string projectId) { string applicationId = $"{csiObject.ObjectType}{csiObject.Name}"; // TODO: NO! Use GUID string sourceType = csiObject.ObjectName; @@ -100,7 +141,6 @@ private SendConversionResult ConvertCSiObject(ICsiWrapper csiObject, Collection } var collection = _sendCollectionManager.AddObjectCollectionToRoot(converted, typeCollection); - collection.elements ??= new List(); collection.elements.Add(converted); return new(Status.SUCCESS, applicationId, sourceType, converted); diff --git a/Connectors/CSi/Speckle.Connectors.CSiShared/ServiceRegistration.cs b/Connectors/CSi/Speckle.Connectors.CSiShared/ServiceRegistration.cs index 9e829d51c..bb37e73ca 100644 --- a/Connectors/CSi/Speckle.Connectors.CSiShared/ServiceRegistration.cs +++ b/Connectors/CSi/Speckle.Connectors.CSiShared/ServiceRegistration.cs @@ -7,12 +7,14 @@ using Speckle.Connectors.CSiShared.Builders; using Speckle.Connectors.CSiShared.Filters; using Speckle.Connectors.CSiShared.HostApp; +using Speckle.Connectors.CSiShared.HostApp.Helpers; using Speckle.Connectors.DUI; using Speckle.Connectors.DUI.Bindings; using Speckle.Connectors.DUI.Bridge; using Speckle.Connectors.DUI.Models.Card.SendFilter; using Speckle.Connectors.DUI.WebView; using Speckle.Converters.CSiShared; +using Speckle.Converters.CSiShared.ToSpeckle.Helpers; namespace Speckle.Connectors.CSiShared; @@ -42,6 +44,14 @@ public static IServiceCollection AddCsi(this IServiceCollection services) services.AddScoped, CsiRootObjectBuilder>(); services.AddScoped>(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + // add converter caches + services.AddScoped(); + return services; } } diff --git a/Connectors/CSi/Speckle.Connectors.CSiShared/Speckle.Connectors.CSiShared.projitems b/Connectors/CSi/Speckle.Connectors.CSiShared/Speckle.Connectors.CSiShared.projitems index 473a2a11b..e407fa234 100644 --- a/Connectors/CSi/Speckle.Connectors.CSiShared/Speckle.Connectors.CSiShared.projitems +++ b/Connectors/CSi/Speckle.Connectors.CSiShared/Speckle.Connectors.CSiShared.projitems @@ -13,7 +13,14 @@ + + + + + + + diff --git a/Connectors/CSi/Speckle.Connectors.ETABS21/Plugin/SpeckleForm.cs b/Connectors/CSi/Speckle.Connectors.ETABS21/Plugin/SpeckleForm.cs index 79043b5ee..349144d0c 100644 --- a/Connectors/CSi/Speckle.Connectors.ETABS21/Plugin/SpeckleForm.cs +++ b/Connectors/CSi/Speckle.Connectors.ETABS21/Plugin/SpeckleForm.cs @@ -8,5 +8,5 @@ namespace Speckle.Connectors.ETABS21; public class SpeckleForm : EtabsSpeckleFormBase { - protected override HostAppVersion GetVersion() => HostAppVersion.v2021; // TODO: We need a v21 + protected override HostAppVersion GetVersion() => HostAppVersion.v21; } diff --git a/Connectors/CSi/Speckle.Connectors.ETABS22/Plugin/SpeckleForm.cs b/Connectors/CSi/Speckle.Connectors.ETABS22/Plugin/SpeckleForm.cs index 920ed8d0f..c25e9b5ca 100644 --- a/Connectors/CSi/Speckle.Connectors.ETABS22/Plugin/SpeckleForm.cs +++ b/Connectors/CSi/Speckle.Connectors.ETABS22/Plugin/SpeckleForm.cs @@ -8,5 +8,5 @@ namespace Speckle.Connectors.ETABS22; public class SpeckleForm : EtabsSpeckleFormBase { - protected override HostAppVersion GetVersion() => HostAppVersion.v2021; // TODO: v22 + protected override HostAppVersion GetVersion() => HostAppVersion.v22; } diff --git a/Connectors/CSi/Speckle.Connectors.ETABSShared/HostApp/EtabsSectionUnpacker.cs b/Connectors/CSi/Speckle.Connectors.ETABSShared/HostApp/EtabsSectionUnpacker.cs new file mode 100644 index 000000000..528974450 --- /dev/null +++ b/Connectors/CSi/Speckle.Connectors.ETABSShared/HostApp/EtabsSectionUnpacker.cs @@ -0,0 +1,110 @@ +using Speckle.Connectors.CSiShared.HostApp.Helpers; +using Speckle.Connectors.ETABSShared.HostApp.Helpers; +using Speckle.Converters.CSiShared.ToSpeckle.Helpers; +using Speckle.Sdk.Models.Proxies; + +namespace Speckle.Connectors.ETABSShared.HostApp; + +/// +/// Unpacks and creates proxies for frame and shell sections from the model. +/// +/// +/// Provides a unified approach to section extraction across different section types. +/// Leverages specialized extractors to handle complex property retrieval. Centralizes +/// section proxy creation with robust error handling and logging mechanisms. +/// +public class EtabsSectionUnpacker : ISectionUnpacker +{ + private readonly EtabsSectionPropertyExtractor _propertyExtractor; + private readonly CsiToSpeckleCacheSingleton _csiToSpeckleCacheSingleton; + + public EtabsSectionUnpacker( + EtabsSectionPropertyExtractor propertyExtractor, + CsiToSpeckleCacheSingleton csiToSpeckleCacheSingleton + ) + { + _propertyExtractor = propertyExtractor; + _csiToSpeckleCacheSingleton = csiToSpeckleCacheSingleton; + } + + public IEnumerable UnpackSections() + { + foreach (GroupProxy frameSectionProxy in UnpackFrameSections()) + { + yield return frameSectionProxy; + } + + foreach (GroupProxy shellSectionProxy in UnpackShellSections()) + { + yield return shellSectionProxy; + } + } + + private IEnumerable UnpackFrameSections() + { + foreach (var entry in _csiToSpeckleCacheSingleton.FrameSectionCache) + { + string sectionName = entry.Key; + List frameIds = entry.Value; + + // Initialize properties outside the if statement + Dictionary properties = new Dictionary(); + + // 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 the section proxy + GroupProxy sectionProxy = + new() + { + id = sectionName, + 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 + }; + + yield return sectionProxy; + } + } + + private IEnumerable UnpackShellSections() + { + foreach (var entry in _csiToSpeckleCacheSingleton.ShellSectionCache) + { + string sectionName = entry.Key; + List frameIds = entry.Value; + + // Initialize properties outside the if statement + Dictionary properties = new Dictionary(); + + // 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.ExtractShellSectionProperties(sectionName); + } + + // create the section proxy + GroupProxy sectionProxy = + new() + { + id = sectionName, + name = sectionName, + applicationId = sectionName, + objects = frameIds, + ["type"] = "Shell Section", // since sectionProxies are a flat list, need some way to distinguish from frame + ["properties"] = properties // openings will just have an empty dict here + }; + + yield return sectionProxy; + } + } +} diff --git a/Connectors/CSi/Speckle.Connectors.ETABSShared/HostApp/EtabsSendCollectionManager.cs b/Connectors/CSi/Speckle.Connectors.ETABSShared/HostApp/EtabsSendCollectionManager.cs index b721c2d18..6f6b88a7d 100644 --- a/Connectors/CSi/Speckle.Connectors.ETABSShared/HostApp/EtabsSendCollectionManager.cs +++ b/Connectors/CSi/Speckle.Connectors.ETABSShared/HostApp/EtabsSendCollectionManager.cs @@ -1,6 +1,7 @@ using Speckle.Connectors.CSiShared.HostApp; using Speckle.Converters.Common; using Speckle.Converters.CSiShared; +using Speckle.Converters.CSiShared.Utils; using Speckle.Sdk.Models; using Speckle.Sdk.Models.Collections; @@ -66,7 +67,10 @@ private ElementCategory GetElementCategoryFromObject(Base obj) } // For frames and shells, get design orientation from Object ID - if ((type == "Frame" || type == "Shell") && obj["properties"] is Dictionary properties) + if ( + (type == ModelObjectType.FRAME.ToString() || type == ModelObjectType.SHELL.ToString()) + && obj["properties"] is Dictionary properties + ) { if (properties.TryGetValue("Object ID", out var objectId) && objectId is Dictionary parameters) { @@ -78,7 +82,7 @@ private ElementCategory GetElementCategoryFromObject(Base obj) } // For joints, simply categorize as joints - return type == "Joint" ? ElementCategory.JOINT : ElementCategory.OTHER; + return type == ModelObjectType.JOINT.ToString() ? ElementCategory.JOINT : ElementCategory.OTHER; } private ElementCategory GetCategoryFromDesignOrientation(string? orientation, string type) @@ -90,12 +94,12 @@ private ElementCategory GetCategoryFromDesignOrientation(string? orientation, st return (orientation, type) switch { - ("Column", "Frame") => ElementCategory.COLUMN, - ("Beam", "Frame") => ElementCategory.BEAM, - ("Brace", "Frame") => ElementCategory.BRACE, - ("Wall", "Shell") => ElementCategory.WALL, - ("Floor", "Shell") => ElementCategory.FLOOR, - ("Ramp", "Shell") => ElementCategory.RAMP, + ("Column", nameof(ModelObjectType.FRAME)) => ElementCategory.COLUMN, + ("Beam", nameof(ModelObjectType.FRAME)) => ElementCategory.BEAM, + ("Brace", nameof(ModelObjectType.FRAME)) => ElementCategory.BRACE, + ("Wall", nameof(ModelObjectType.SHELL)) => ElementCategory.WALL, + ("Floor", nameof(ModelObjectType.SHELL)) => ElementCategory.FLOOR, + ("Ramp", nameof(ModelObjectType.SHELL)) => ElementCategory.RAMP, _ => ElementCategory.OTHER }; } @@ -138,16 +142,4 @@ private Collection CreateCategoryCollection(ElementCategory category, Collection levelCollection.elements.Add(categoryCollection); return categoryCollection; } - - private enum ElementCategory - { - COLUMN, - BEAM, - BRACE, - WALL, - FLOOR, - RAMP, - JOINT, - OTHER - } } diff --git a/Connectors/CSi/Speckle.Connectors.ETABSShared/HostApp/Helpers/EtabsFrameSectionPropertyExtractor.cs b/Connectors/CSi/Speckle.Connectors.ETABSShared/HostApp/Helpers/EtabsFrameSectionPropertyExtractor.cs new file mode 100644 index 000000000..2663bd582 --- /dev/null +++ b/Connectors/CSi/Speckle.Connectors.ETABSShared/HostApp/Helpers/EtabsFrameSectionPropertyExtractor.cs @@ -0,0 +1,78 @@ +using Speckle.Connectors.CSiShared.HostApp.Helpers; +using Speckle.Converters.Common; +using Speckle.Converters.CSiShared; +using Speckle.Converters.CSiShared.Utils; + +namespace Speckle.Connectors.ETABSShared.HostApp.Helpers; + +/// +/// Extracts ETABS-specific frame section properties. +/// +public class EtabsFrameSectionPropertyExtractor : IApplicationFrameSectionPropertyExtractor +{ + private readonly IConverterSettingsStore _settingsStore; + + public EtabsFrameSectionPropertyExtractor(IConverterSettingsStore settingsStore) + { + _settingsStore = settingsStore; + } + + /// + /// Gets generalised frame section properties + /// + /// + /// 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. + /// + public void ExtractProperties(string sectionName, Dictionary 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) + { + // General Data + var generalData = DictionaryUtils.EnsureNestedDictionary(properties, SectionPropertyCategory.GENERAL_DATA); + generalData["type"] = propTypes[sectionIndex].ToString(); + + // Section Dimensions + var sectionDimensions = DictionaryUtils.EnsureNestedDictionary( + properties, + SectionPropertyCategory.SECTION_DIMENSIONS + ); + sectionDimensions["t3"] = t3[sectionIndex]; + sectionDimensions["t2"] = t2[sectionIndex]; + sectionDimensions["tf"] = tf[sectionIndex]; + sectionDimensions["tw"] = tw[sectionIndex]; + sectionDimensions["t2b"] = t2b[sectionIndex]; + sectionDimensions["tfb"] = tfb[sectionIndex]; + sectionDimensions["area"] = area[sectionIndex]; + } + } +} diff --git a/Connectors/CSi/Speckle.Connectors.ETABSShared/HostApp/Helpers/EtabsSectionPropertyExtractor.cs b/Connectors/CSi/Speckle.Connectors.ETABSShared/HostApp/Helpers/EtabsSectionPropertyExtractor.cs new file mode 100644 index 000000000..93e0f3d30 --- /dev/null +++ b/Connectors/CSi/Speckle.Connectors.ETABSShared/HostApp/Helpers/EtabsSectionPropertyExtractor.cs @@ -0,0 +1,53 @@ +using Speckle.Connectors.CSiShared.HostApp.Helpers; + +namespace Speckle.Connectors.ETABSShared.HostApp.Helpers; + +/// +/// Coordinates property extraction combining base CSi and ETABS-specific properties. +/// +/// +/// Mirrors property extraction system pattern used in EtabsPropertiesExtractor. +/// Composition handled at coordinator level rather than individual extractors. +/// +public class EtabsSectionPropertyExtractor +{ + private readonly IFrameSectionPropertyExtractor _csiFrameExtractor; + private readonly IShellSectionPropertyExtractor _csiShellExtractor; + private readonly IApplicationFrameSectionPropertyExtractor _etabsFrameExtractor; + private readonly IApplicationShellSectionPropertyExtractor _etabsShellExtractor; + + public EtabsSectionPropertyExtractor( + IFrameSectionPropertyExtractor csiFrameExtractor, + IShellSectionPropertyExtractor csiShellExtractor, + IApplicationFrameSectionPropertyExtractor etabsFrameExtractor, + IApplicationShellSectionPropertyExtractor etabsShellExtractor + ) + { + _csiFrameExtractor = csiFrameExtractor; + _csiShellExtractor = csiShellExtractor; + _etabsFrameExtractor = etabsFrameExtractor; + _etabsShellExtractor = etabsShellExtractor; + } + + /// + /// Extract the frame section properties on both a Csi and app-specific level + /// + public Dictionary ExtractFrameSectionProperties(string sectionName) + { + Dictionary properties = new(); + _csiFrameExtractor.ExtractProperties(sectionName, properties); + _etabsFrameExtractor.ExtractProperties(sectionName, properties); + return properties; + } + + /// + /// Extract the shell section properties on both a Csi and app-specific level + /// + public Dictionary ExtractShellSectionProperties(string sectionName) + { + Dictionary properties = new(); + _csiShellExtractor.ExtractProperties(sectionName, properties); + _etabsShellExtractor.ExtractProperties(sectionName, properties); + return properties; + } +} diff --git a/Connectors/CSi/Speckle.Connectors.ETABSShared/HostApp/Helpers/EtabsShellSectionPropertyExtractor.cs b/Connectors/CSi/Speckle.Connectors.ETABSShared/HostApp/Helpers/EtabsShellSectionPropertyExtractor.cs new file mode 100644 index 000000000..d4506f0ba --- /dev/null +++ b/Connectors/CSi/Speckle.Connectors.ETABSShared/HostApp/Helpers/EtabsShellSectionPropertyExtractor.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.Logging; +using Speckle.Connectors.CSiShared.HostApp.Helpers; +using Speckle.Converters.Common; +using Speckle.Converters.CSiShared; +using Speckle.Converters.CSiShared.Utils; + +namespace Speckle.Connectors.ETABSShared.HostApp.Helpers; + +/// +/// Extracts ETABS-specific shell section properties. +/// +public class EtabsShellSectionPropertyExtractor : IApplicationShellSectionPropertyExtractor +{ + private readonly IConverterSettingsStore _settingsStore; + private readonly ILogger _logger; + private readonly EtabsShellSectionResolver _etabsShellSectionResolver; + + public EtabsShellSectionPropertyExtractor( + IConverterSettingsStore settingsStore, + ILogger logger, + EtabsShellSectionResolver etabsShellSectionResolver + ) + { + _settingsStore = settingsStore; + _logger = logger; + _etabsShellSectionResolver = etabsShellSectionResolver; + } + + /// + /// Extract shell section properties + /// + /// + /// sectionName is unique across all types (Wall, Slab and Deck) + /// There is no general query such as PropArea.GetShell() - rather we have to be specific on the type, for example + /// PropArea.GetWall() or PropArea.GetDeck() BUT we can't get the building type given a SectionName. + /// Hence the introduction of ResolveSection. + /// + public void ExtractProperties(string sectionName, Dictionary properties) + { + // Step 01: Finding the appropriate api query for the unknown section type (wall, deck or slab) + Dictionary resolvedProperties = _etabsShellSectionResolver.ResolveSection(sectionName); + + // Step 02: Mutate properties dictionary with resolved properties + foreach (var nestedDictionary in resolvedProperties) + { + if (nestedDictionary.Value is not Dictionary nestedValues) + { + _logger.LogWarning( + "Unexpected value type for key {Key} in section {SectionName}. Expected Dictionary, got {ActualType}", + nestedDictionary.Key, + sectionName, + nestedDictionary.Value?.GetType().Name ?? "null" + ); + continue; + } + + var nestedProperties = DictionaryUtils.EnsureNestedDictionary(properties, nestedDictionary.Key); + foreach (var kvp in nestedValues) + { + nestedProperties[kvp.Key] = kvp.Value; + } + } + } +} diff --git a/Connectors/CSi/Speckle.Connectors.ETABSShared/HostApp/Helpers/EtabsShellSectionResolver.cs b/Connectors/CSi/Speckle.Connectors.ETABSShared/HostApp/Helpers/EtabsShellSectionResolver.cs new file mode 100644 index 000000000..5d54a0482 --- /dev/null +++ b/Connectors/CSi/Speckle.Connectors.ETABSShared/HostApp/Helpers/EtabsShellSectionResolver.cs @@ -0,0 +1,182 @@ +using Speckle.Converters.Common; +using Speckle.Converters.CSiShared; +using Speckle.Converters.CSiShared.Utils; + +namespace Speckle.Connectors.ETABSShared.HostApp.Helpers; + +/// +/// Attempts to resolve the section type and retrieve its properties by trying different section resolvers. +/// +/// +/// This service focuses solely on determining the correct section type and returning its properties. +/// Since section names are unique across different types (Wall, Slab, Deck), it uses a try-and-fail approach +/// rather than attempting to predetermine the type. The first successful resolution is returned. +/// +public record AreaSectionResult +{ + public bool Success { get; init; } + public Dictionary Properties { get; init; } +} + +public interface IAreaSectionResolver +{ + AreaSectionResult TryResolveSection(string sectionName); +} + +public class EtabsShellSectionResolver +{ + private readonly IConverterSettingsStore _settingsStore; + private readonly IEnumerable _resolvers; + + public EtabsShellSectionResolver(IConverterSettingsStore settingsStore) + { + _settingsStore = settingsStore; + _resolvers = + [ + new WallSectionResolver(_settingsStore), + new SlabSectionResolver(_settingsStore), + new DeckSectionResolver(_settingsStore) + ]; + } + + public Dictionary ResolveSection(string sectionName) + { + foreach (var resolver in _resolvers) + { + var result = resolver.TryResolveSection(sectionName); + if (result.Success) + { + return result.Properties; + } + } + + throw new InvalidOperationException($"Section '{sectionName}' could not be resolved to any known type."); + } +} + +public class WallSectionResolver(IConverterSettingsStore settingsStore) : IAreaSectionResolver +{ + public AreaSectionResult TryResolveSection(string sectionName) + { + eWallPropType wallPropType = default; + eShellType shellType = default; + string matProp = string.Empty; + double thickness = 0.0; + int color = 0; + string notes = string.Empty; + string guid = string.Empty; + + var result = settingsStore.Current.SapModel.PropArea.GetWall( + sectionName, + ref wallPropType, + ref shellType, + ref matProp, + ref thickness, + ref color, + ref notes, + ref guid + ); + + Dictionary generalData = []; + generalData["name"] = sectionName; + generalData["type"] = wallPropType.ToString(); + generalData["material"] = matProp; + generalData["modelingType"] = shellType.ToString(); + generalData["color"] = color; + generalData["notes"] = notes; + + Dictionary propertyData = []; + propertyData["type"] = "Wall"; + propertyData["thickness"] = thickness; + + Dictionary properties = []; + properties[SectionPropertyCategory.GENERAL_DATA] = generalData; + properties[SectionPropertyCategory.PROPERTY_DATA] = propertyData; + + return new AreaSectionResult { Success = result == 0, Properties = properties }; + } +} + +public class SlabSectionResolver(IConverterSettingsStore settingsStore) : IAreaSectionResolver +{ + public AreaSectionResult TryResolveSection(string sectionName) + { + eSlabType slabType = default; + eShellType shellType = default; + string matProp = string.Empty; + double thickness = 0.0; + int color = 0; + string notes = string.Empty; + string guid = string.Empty; + + var result = settingsStore.Current.SapModel.PropArea.GetSlab( + sectionName, + ref slabType, + ref shellType, + ref matProp, + ref thickness, + ref color, + ref notes, + ref guid + ); + + Dictionary generalData = []; + generalData["name"] = sectionName; + generalData["material"] = matProp; + generalData["modelingType"] = shellType.ToString(); + generalData["color"] = color; + generalData["notes"] = notes; + + Dictionary propertyData = []; + propertyData["type"] = slabType.ToString(); + propertyData["thickness"] = thickness; + + Dictionary properties = []; + properties[SectionPropertyCategory.GENERAL_DATA] = generalData; + properties[SectionPropertyCategory.PROPERTY_DATA] = propertyData; + + return new AreaSectionResult { Success = result == 0, Properties = properties }; + } +} + +public class DeckSectionResolver(IConverterSettingsStore settingsStore) : IAreaSectionResolver +{ + public AreaSectionResult TryResolveSection(string sectionName) + { + eDeckType deckType = default; + eShellType shellType = default; + string deckMatProp = string.Empty; + double thickness = 0.0; + int color = 0; + string notes = string.Empty; + string guid = string.Empty; + + var result = settingsStore.Current.SapModel.PropArea.GetDeck( + sectionName, + ref deckType, + ref shellType, + ref deckMatProp, + ref thickness, + ref color, + ref notes, + ref guid + ); + + Dictionary generalData = []; + generalData["name"] = sectionName; + generalData["type"] = deckType.ToString(); + generalData["material"] = deckMatProp; + generalData["modelingType"] = shellType.ToString(); + generalData["color"] = color; + generalData["notes"] = notes; + + Dictionary propertyData = []; + propertyData["thickness"] = thickness; + + Dictionary properties = []; + properties[SectionPropertyCategory.GENERAL_DATA] = generalData; + properties[SectionPropertyCategory.PROPERTY_DATA] = propertyData; + + return new AreaSectionResult { Success = result == 0, Properties = properties }; + } +} diff --git a/Connectors/CSi/Speckle.Connectors.ETABSShared/ServiceRegistration.cs b/Connectors/CSi/Speckle.Connectors.ETABSShared/ServiceRegistration.cs index b4fcbc991..871a32cd6 100644 --- a/Connectors/CSi/Speckle.Connectors.ETABSShared/ServiceRegistration.cs +++ b/Connectors/CSi/Speckle.Connectors.ETABSShared/ServiceRegistration.cs @@ -1,6 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Speckle.Connectors.CSiShared.HostApp; +using Speckle.Connectors.CSiShared.HostApp.Helpers; using Speckle.Connectors.ETABSShared.HostApp; +using Speckle.Connectors.ETABSShared.HostApp.Helpers; using Speckle.Converters.ETABSShared; namespace Speckle.Connectors.ETABSShared; @@ -10,7 +12,12 @@ public static class ServiceRegistration public static IServiceCollection AddEtabs(this IServiceCollection services) { services.AddEtabsConverters(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/Connectors/CSi/Speckle.Connectors.ETABSShared/Speckle.Connectors.ETABSShared.projitems b/Connectors/CSi/Speckle.Connectors.ETABSShared/Speckle.Connectors.ETABSShared.projitems index badbea3c9..d6f11f0f8 100644 --- a/Connectors/CSi/Speckle.Connectors.ETABSShared/Speckle.Connectors.ETABSShared.projitems +++ b/Connectors/CSi/Speckle.Connectors.ETABSShared/Speckle.Connectors.ETABSShared.projitems @@ -9,7 +9,12 @@ Speckle.Connectors.ETABSShared + + + + + diff --git a/Converters/CSi/Speckle.Converters.CSiShared/CsiWrappers.cs b/Converters/CSi/Speckle.Converters.CSiShared/CsiWrappers.cs index 63d01448a..ab5b02da0 100644 --- a/Converters/CSi/Speckle.Converters.CSiShared/CsiWrappers.cs +++ b/Converters/CSi/Speckle.Converters.CSiShared/CsiWrappers.cs @@ -1,9 +1,11 @@ +using Speckle.Converters.CSiShared.Utils; + namespace Speckle.Converters.CSiShared; public interface ICsiWrapper { string Name { get; set; } - int ObjectType { get; } + ModelObjectType ObjectType { get; } string ObjectName { get; } } @@ -18,50 +20,50 @@ public interface ICsiWrapper public abstract class CsiWrapperBase : ICsiWrapper { public required string Name { get; set; } - public abstract int ObjectType { get; } + public abstract ModelObjectType ObjectType { get; } public abstract string ObjectName { get; } } public class CsiJointWrapper : CsiWrapperBase { - public override int ObjectType => 1; - public override string ObjectName => "Joint"; + public override ModelObjectType ObjectType => ModelObjectType.JOINT; + public override string ObjectName => ModelObjectType.JOINT.ToString(); } public class CsiFrameWrapper : CsiWrapperBase { - public override int ObjectType => 2; - public override string ObjectName => "Frame"; + public override ModelObjectType ObjectType => ModelObjectType.FRAME; + public override string ObjectName => ModelObjectType.FRAME.ToString(); } public class CsiCableWrapper : CsiWrapperBase { - public override int ObjectType => 3; - public override string ObjectName => "Cable"; + public override ModelObjectType ObjectType => ModelObjectType.CABLE; + public override string ObjectName => ModelObjectType.CABLE.ToString(); } public class CsiTendonWrapper : CsiWrapperBase { - public override int ObjectType => 4; - public override string ObjectName => "Tendon"; + public override ModelObjectType ObjectType => ModelObjectType.TENDON; + public override string ObjectName => ModelObjectType.TENDON.ToString(); } public class CsiShellWrapper : CsiWrapperBase { - public override int ObjectType => 5; - public override string ObjectName => "Shell"; + public override ModelObjectType ObjectType => ModelObjectType.SHELL; + public override string ObjectName => ModelObjectType.SHELL.ToString(); } public class CsiSolidWrapper : CsiWrapperBase { - public override int ObjectType => 6; - public override string ObjectName => "Solid"; + public override ModelObjectType ObjectType => ModelObjectType.SOLID; + public override string ObjectName => ModelObjectType.SOLID.ToString(); } public class CsiLinkWrapper : CsiWrapperBase { - public override int ObjectType => 7; - public override string ObjectName => "Link"; + public override ModelObjectType ObjectType => ModelObjectType.LINK; + public override string ObjectName => ModelObjectType.LINK.ToString(); } /// diff --git a/Converters/CSi/Speckle.Converters.CSiShared/ServiceRegistration.cs b/Converters/CSi/Speckle.Converters.CSiShared/ServiceRegistration.cs index 5928293d7..6444e7a93 100644 --- a/Converters/CSi/Speckle.Converters.CSiShared/ServiceRegistration.cs +++ b/Converters/CSi/Speckle.Converters.CSiShared/ServiceRegistration.cs @@ -23,6 +23,9 @@ public static IServiceCollection AddCsiConverters(this IServiceCollection servic serviceCollection.AddScoped(); serviceCollection.AddScoped(); + // Register connector caches + serviceCollection.AddScoped(); + // Settings and unit conversions serviceCollection.AddApplicationConverters(converterAssembly); serviceCollection.AddScoped< diff --git a/Converters/CSi/Speckle.Converters.CSiShared/Speckle.Converters.CSiShared.projitems b/Converters/CSi/Speckle.Converters.CSiShared/Speckle.Converters.CSiShared.projitems index 7d59d119a..b899a60dc 100644 --- a/Converters/CSi/Speckle.Converters.CSiShared/Speckle.Converters.CSiShared.projitems +++ b/Converters/CSi/Speckle.Converters.CSiShared/Speckle.Converters.CSiShared.projitems @@ -20,6 +20,7 @@ + @@ -27,6 +28,8 @@ + + \ No newline at end of file diff --git a/Converters/CSi/Speckle.Converters.CSiShared/ToSpeckle/Helpers/CsiFramePropertiesExtractor.cs b/Converters/CSi/Speckle.Converters.CSiShared/ToSpeckle/Helpers/CsiFramePropertiesExtractor.cs index 63be4e657..d90367481 100644 --- a/Converters/CSi/Speckle.Converters.CSiShared/ToSpeckle/Helpers/CsiFramePropertiesExtractor.cs +++ b/Converters/CSi/Speckle.Converters.CSiShared/ToSpeckle/Helpers/CsiFramePropertiesExtractor.cs @@ -21,6 +21,9 @@ namespace Speckle.Converters.CSiShared.ToSpeckle.Helpers; public sealed class CsiFramePropertiesExtractor { private readonly IConverterSettingsStore _settingsStore; + + private readonly CsiToSpeckleCacheSingleton _csiToSpeckleCacheSingleton; + private static readonly string[] s_releaseKeys = [ "axial", @@ -31,8 +34,12 @@ public sealed class CsiFramePropertiesExtractor "majorBending" ]; // Note: caching keys for better performance - public CsiFramePropertiesExtractor(IConverterSettingsStore settingsStore) + public CsiFramePropertiesExtractor( + CsiToSpeckleCacheSingleton csiToSpeckleCacheSingleton, + IConverterSettingsStore settingsStore + ) { + _csiToSpeckleCacheSingleton = csiToSpeckleCacheSingleton; _settingsStore = settingsStore; } @@ -40,17 +47,53 @@ public void ExtractProperties(CsiFrameWrapper frame, PropertyExtractionResult fr { frameData.ApplicationId = frame.GetSpeckleApplicationId(_settingsStore.Current.SapModel); - var geometry = DictionaryUtils.EnsureNestedDictionary(frameData.Properties, "Geometry"); + var geometry = DictionaryUtils.EnsureNestedDictionary(frameData.Properties, ObjectPropertyCategory.GEOMETRY); (geometry["startJointName"], geometry["endJointName"]) = GetEndPointNames(frame); - var assignments = DictionaryUtils.EnsureNestedDictionary(frameData.Properties, "Assignments"); + var assignments = DictionaryUtils.EnsureNestedDictionary(frameData.Properties, ObjectPropertyCategory.ASSIGNMENTS); assignments["groups"] = new List(GetGroupAssigns(frame)); assignments["materialOverwrite"] = GetMaterialOverwrite(frame); assignments["localAxis"] = GetLocalAxes(frame); assignments["propertyModifiers"] = GetModifiers(frame); assignments["endReleases"] = GetReleases(frame); - assignments["sectionProperty"] = GetSectionName(frame); assignments["path"] = GetPathType(frame); + + // NOTE: sectionId and materialId a "quick-fix" to enable filtering in the viewer etc. + // Assign sectionId to variable as this will be an argument for the GetMaterialName method + string sectionId = GetSectionName(frame); + string materialId = GetMaterialName(sectionId); + assignments[ObjectPropertyKey.SECTION_ID] = sectionId; + assignments[ObjectPropertyKey.MATERIAL_ID] = materialId; + + // store the object, section, and material id relationships in their corresponding caches to be accessed by the connector + if (!string.IsNullOrEmpty(sectionId)) + { + if (_csiToSpeckleCacheSingleton.FrameSectionCache.TryGetValue(sectionId, out List? frameIds)) + { + frameIds.Add(frameData.ApplicationId); + } + else + { + _csiToSpeckleCacheSingleton.FrameSectionCache.Add(sectionId, new List() { frameData.ApplicationId }); + } + + if (!string.IsNullOrEmpty(materialId)) + { + if (_csiToSpeckleCacheSingleton.MaterialCache.TryGetValue(materialId, out List? sectionIds)) + { + // Since this is happening on the object level, we could be processing the same sectionIds (from different + // objects) many times. This is not necessary since we just want a set of sectionId corresponding to material + if (!sectionIds.Contains(sectionId)) + { + sectionIds.Add(sectionId); + } + } + else + { + _csiToSpeckleCacheSingleton.MaterialCache.Add(materialId, new List() { sectionId }); + } + } + } } private string[] GetGroupAssigns(CsiFrameWrapper frame) @@ -71,14 +114,14 @@ private string[] GetGroupAssigns(CsiFrameWrapper frame) private string GetMaterialOverwrite(CsiFrameWrapper frame) { - string propName = "None"; + string propName = string.Empty; _ = _settingsStore.Current.SapModel.FrameObj.GetMaterialOverwrite(frame.Name, ref propName); return propName; } private Dictionary GetModifiers(CsiFrameWrapper frame) { - double[] value = Array.Empty(); + double[] value = []; _ = _settingsStore.Current.SapModel.FrameObj.GetModifiers(frame.Name, ref value); return new Dictionary { @@ -88,8 +131,8 @@ private string GetMaterialOverwrite(CsiFrameWrapper frame) ["torsionalConstantModifier"] = value[3], ["momentOfInertiaAboutLocal2AxisModifier"] = value[4], ["momentOfInertiaAboutLocal3AxisModifier"] = value[5], - ["massModifier"] = value[6], - ["weightModifier"] = value[7] + ["mass"] = value[6], + ["weight"] = value[7] }; } @@ -103,10 +146,10 @@ private string GetMaterialOverwrite(CsiFrameWrapper frame) private Dictionary GetReleases(CsiFrameWrapper frame) { - bool[] ii = Array.Empty(), - jj = Array.Empty(); - double[] startValue = Array.Empty(), - endValue = Array.Empty(); + bool[] ii = [], + jj = []; + double[] startValue = [], + endValue = []; _ = _settingsStore.Current.SapModel.FrameObj.GetReleases(frame.Name, ref ii, ref jj, ref startValue, ref endValue); @@ -147,4 +190,13 @@ private string GetPathType(CsiFrameWrapper frame) _ = _settingsStore.Current.SapModel.FrameObj.GetTypeOAPI(frame.Name, ref pathType); return pathType; } + + // NOTE: This is a little convoluted as we aren't on the cFrameObj level, but one deeper. + // As noted in ExtractProperties, this is just a quick-fix to get some displayable materialId parameter + private string GetMaterialName(string sectionName) + { + string materialName = string.Empty; + _ = _settingsStore.Current.SapModel.PropFrame.GetMaterial(sectionName, ref materialName); + return materialName; + } } diff --git a/Converters/CSi/Speckle.Converters.CSiShared/ToSpeckle/Helpers/CsiJointPropertiesExtractor.cs b/Converters/CSi/Speckle.Converters.CSiShared/ToSpeckle/Helpers/CsiJointPropertiesExtractor.cs index 5b3baf53c..600a78e6f 100644 --- a/Converters/CSi/Speckle.Converters.CSiShared/ToSpeckle/Helpers/CsiJointPropertiesExtractor.cs +++ b/Converters/CSi/Speckle.Converters.CSiShared/ToSpeckle/Helpers/CsiJointPropertiesExtractor.cs @@ -34,7 +34,7 @@ public void ExtractProperties(CsiJointWrapper joint, PropertyExtractionResult jo { jointData.ApplicationId = joint.GetSpeckleApplicationId(_settingsStore.Current.SapModel); - var assignments = DictionaryUtils.EnsureNestedDictionary(jointData.Properties, "Assignments"); + var assignments = DictionaryUtils.EnsureNestedDictionary(jointData.Properties, ObjectPropertyCategory.ASSIGNMENTS); assignments["groups"] = new List(GetGroupAssigns(joint)); assignments["restraints"] = GetRestraints(joint); } @@ -49,7 +49,7 @@ private string[] GetGroupAssigns(CsiJointWrapper joint) private Dictionary GetRestraints(CsiJointWrapper joint) { - bool[] restraints = Array.Empty(); + bool[] restraints = []; _ = _settingsStore.Current.SapModel.PointObj.GetRestraint(joint.Name, ref restraints); return new Dictionary { diff --git a/Converters/CSi/Speckle.Converters.CSiShared/ToSpeckle/Helpers/CsiShellPropertiesExtractor.cs b/Converters/CSi/Speckle.Converters.CSiShared/ToSpeckle/Helpers/CsiShellPropertiesExtractor.cs index 1e5310e97..f2bb6480c 100644 --- a/Converters/CSi/Speckle.Converters.CSiShared/ToSpeckle/Helpers/CsiShellPropertiesExtractor.cs +++ b/Converters/CSi/Speckle.Converters.CSiShared/ToSpeckle/Helpers/CsiShellPropertiesExtractor.cs @@ -31,15 +31,14 @@ public void ExtractProperties(CsiShellWrapper shell, PropertyExtractionResult sh { shellData.ApplicationId = shell.GetSpeckleApplicationId(_settingsStore.Current.SapModel); - var geometry = DictionaryUtils.EnsureNestedDictionary(shellData.Properties, "Geometry"); + var geometry = DictionaryUtils.EnsureNestedDictionary(shellData.Properties, ObjectPropertyCategory.GEOMETRY); geometry["shellVerticesJointNames"] = GetPointNames(shell); - var assignments = DictionaryUtils.EnsureNestedDictionary(shellData.Properties, "Assignments"); + var assignments = DictionaryUtils.EnsureNestedDictionary(shellData.Properties, ObjectPropertyCategory.ASSIGNMENTS); assignments["groups"] = new List(GetGroupAssigns(shell)); assignments["localAxis"] = GetLocalAxes(shell); assignments["materialOverwrite"] = GetMaterialOverwrite(shell); assignments["propertyModifiers"] = GetModifiers(shell); - assignments["sectionProperty"] = GetSectionName(shell); } private string[] GetGroupAssigns(CsiShellWrapper shell) @@ -60,42 +59,35 @@ private string[] GetGroupAssigns(CsiShellWrapper shell) private string GetMaterialOverwrite(CsiShellWrapper shell) { - string propName = "None"; + string propName = string.Empty; _ = _settingsStore.Current.SapModel.AreaObj.GetMaterialOverwrite(shell.Name, ref propName); return propName; } private Dictionary GetModifiers(CsiShellWrapper shell) { - double[] value = Array.Empty(); + double[] value = []; _ = _settingsStore.Current.SapModel.AreaObj.GetModifiers(shell.Name, ref value); return new Dictionary { - ["membraneF11Modifier"] = value[0], - ["membraneF22Modifier"] = value[1], - ["membraneF12Modifier"] = value[2], - ["bendingM11Modifier"] = value[3], - ["bendingM22Modifier"] = value[4], - ["bendingM12Modifier"] = value[5], - ["shearV13Modifier"] = value[6], - ["shearV23Modifier"] = value[7], - ["massModifier"] = value[8], - ["weightModifier"] = value[9] + ["f11"] = value[0], + ["f22"] = value[1], + ["f12"] = value[2], + ["m11"] = value[3], + ["m22"] = value[4], + ["m12"] = value[5], + ["v13"] = value[6], + ["v23"] = value[7], + ["mass"] = value[8], + ["weight"] = value[9] }; } private string[] GetPointNames(CsiShellWrapper shell) { int numberPoints = 0; - string[] pointNames = Array.Empty(); + string[] pointNames = []; _ = _settingsStore.Current.SapModel.AreaObj.GetPoints(shell.Name, ref numberPoints, ref pointNames); return pointNames; } - - private string GetSectionName(CsiShellWrapper shell) - { - string sectionName = string.Empty; - _ = _settingsStore.Current.SapModel.AreaObj.GetProperty(shell.Name, ref sectionName); - return sectionName; - } } diff --git a/Converters/CSi/Speckle.Converters.CSiShared/ToSpeckle/Helpers/CsiToSpeckleCacheSingleton.cs b/Converters/CSi/Speckle.Converters.CSiShared/ToSpeckle/Helpers/CsiToSpeckleCacheSingleton.cs new file mode 100644 index 000000000..c192dc39f --- /dev/null +++ b/Converters/CSi/Speckle.Converters.CSiShared/ToSpeckle/Helpers/CsiToSpeckleCacheSingleton.cs @@ -0,0 +1,19 @@ +namespace Speckle.Converters.CSiShared.ToSpeckle.Helpers; + +public class CsiToSpeckleCacheSingleton +{ + /// + /// A map of (material id, section ids). Assumes the material id is the unique name of the material + /// + public Dictionary> MaterialCache { get; set; } = new(); + + /// + /// A map of (section id, frame object id). Assumes the section id is the unique name of the section + /// + public Dictionary> FrameSectionCache { get; set; } = new(); + + /// + /// A map of (section id, shell object id). Assumes the section id is the unique name of the section + /// + public Dictionary> ShellSectionCache { get; set; } = new(); +} diff --git a/Converters/CSi/Speckle.Converters.CSiShared/ToSpeckle/Helpers/IApplicationPropertiesExtractor.cs b/Converters/CSi/Speckle.Converters.CSiShared/ToSpeckle/Helpers/IApplicationPropertiesExtractor.cs index 51e7524f3..1dbb03e2e 100644 --- a/Converters/CSi/Speckle.Converters.CSiShared/ToSpeckle/Helpers/IApplicationPropertiesExtractor.cs +++ b/Converters/CSi/Speckle.Converters.CSiShared/ToSpeckle/Helpers/IApplicationPropertiesExtractor.cs @@ -1,6 +1,6 @@ namespace Speckle.Converters.CSiShared.ToSpeckle.Helpers; -public struct PropertyExtractionResult +public class PropertyExtractionResult { public string Name { get; set; } public string Type { get; set; } diff --git a/Converters/CSi/Speckle.Converters.CSiShared/Utils/Constants.cs b/Converters/CSi/Speckle.Converters.CSiShared/Utils/Constants.cs new file mode 100644 index 000000000..a8794e381 --- /dev/null +++ b/Converters/CSi/Speckle.Converters.CSiShared/Utils/Constants.cs @@ -0,0 +1,34 @@ +namespace Speckle.Converters.CSiShared.Utils; + +// NOTE: Space for string consts used across multiple files and in multiple contexts +// Separate onto dedicated files if this gets too long + +/// +/// These categories mirror the UI. Nested within the properties are the repeated categories +/// This happens across frame, shell and joint objects. +/// The consts formalise the assignable property categories and reduce typos. +/// +public static class ObjectPropertyCategory +{ + public const string ASSIGNMENTS = "Assignments"; + public const string DESIGN = "Design"; + public const string GEOMETRY = "Geometry"; + public const string OBJECT_ID = "Object ID"; +} + +/// +/// These strings are repeatedly used as keys when building the properties dictionary for objects +/// +public static class ObjectPropertyKey +{ + public const string MATERIAL_ID = "materialId"; + public const string SECTION_ID = "sectionId"; +} + +public static class SectionPropertyCategory +{ + public const string GENERAL_DATA = "General Data"; + public const string SECTION_PROPERTIES = "Section Properties"; + public const string SECTION_DIMENSIONS = "Section Dimensions"; + public const string PROPERTY_DATA = "Property Data"; +} diff --git a/Converters/CSi/Speckle.Converters.CSiShared/Utils/DictionaryUtils.cs b/Converters/CSi/Speckle.Converters.CSiShared/Utils/DictionaryUtils.cs index 257e7e5dd..8dbcf0cc8 100644 --- a/Converters/CSi/Speckle.Converters.CSiShared/Utils/DictionaryUtils.cs +++ b/Converters/CSi/Speckle.Converters.CSiShared/Utils/DictionaryUtils.cs @@ -9,25 +9,14 @@ public static class DictionaryUtils /// Ensures a nested dictionary exists at the specified key, creating it if necessary. /// Used for organizing properties into hierarchical categories (e.g., "Geometry", "Assignments", "Design"). /// - /// The parent dictionary to check or modify - /// The key where the nested dictionary should exist - /// - /// The existing nested dictionary if present, or a new empty dictionary after adding it to the parent - /// /// - /// Common usage: - /// - /// var geometry = DictionaryUtils.EnsureNestedDictionary(properties, "Geometry"); - /// geometry["startPoint"] = startPoint; - /// geometry["endPoint"] = endPoint; - /// /// This pattern is used throughout property extractors to maintain consistent property organization. /// public static Dictionary EnsureNestedDictionary(Dictionary dictionary, string key) { if (!dictionary.TryGetValue(key, out var obj) || obj is not Dictionary nestedDictionary) { - nestedDictionary = new Dictionary(); + nestedDictionary = []; dictionary[key] = nestedDictionary; } diff --git a/Converters/CSi/Speckle.Converters.CSiShared/Utils/Enums.cs b/Converters/CSi/Speckle.Converters.CSiShared/Utils/Enums.cs new file mode 100644 index 000000000..78e36e495 --- /dev/null +++ b/Converters/CSi/Speckle.Converters.CSiShared/Utils/Enums.cs @@ -0,0 +1,42 @@ +namespace Speckle.Converters.CSiShared.Utils; + +// NOTE: Should number of enums become too large -> dedicated files. +public enum ModelObjectType +{ + NONE = 0, + JOINT = 1, + FRAME = 2, + CABLE = 3, + TENDON = 4, + SHELL = 5, + SOLID = 6, + LINK = 7 +} + +public enum ElementCategory +{ + COLUMN, + BEAM, + BRACE, + WALL, + FLOOR, + RAMP, + JOINT, + OTHER +} + +public enum DirectionalSymmetryType +{ + ISOTROPIC, + ORTHOTROPIC, + ANISOTROPIC, + UNIAXIAL +} + +public enum AreaPropertyType +{ + NONE = 0, + SHELL = 1, + PLANE = 2, + ASOLID = 3 +} diff --git a/Converters/CSi/Speckle.Converters.ETABSShared/ToSpeckle/Helpers/EtabsFramePropertiesExtractor.cs b/Converters/CSi/Speckle.Converters.ETABSShared/ToSpeckle/Helpers/EtabsFramePropertiesExtractor.cs index e7aa57f56..3e4b899dd 100644 --- a/Converters/CSi/Speckle.Converters.ETABSShared/ToSpeckle/Helpers/EtabsFramePropertiesExtractor.cs +++ b/Converters/CSi/Speckle.Converters.ETABSShared/ToSpeckle/Helpers/EtabsFramePropertiesExtractor.cs @@ -33,14 +33,14 @@ public EtabsFramePropertiesExtractor(IConverterSettingsStore properties) { - var objectId = DictionaryUtils.EnsureNestedDictionary(properties, "Object ID"); + var objectId = DictionaryUtils.EnsureNestedDictionary(properties, ObjectPropertyCategory.OBJECT_ID); objectId["designOrientation"] = GetDesignOrientation(frame); (objectId["label"], objectId["level"]) = GetLabelAndLevel(frame); - var assignments = DictionaryUtils.EnsureNestedDictionary(properties, "Assignments"); + var assignments = DictionaryUtils.EnsureNestedDictionary(properties, ObjectPropertyCategory.ASSIGNMENTS); assignments["springAssignment"] = GetSpringAssignmentName(frame); - var design = DictionaryUtils.EnsureNestedDictionary(properties, "Design"); + var design = DictionaryUtils.EnsureNestedDictionary(properties, ObjectPropertyCategory.DESIGN); design["designProcedure"] = GetDesignProcedure(frame); } diff --git a/Converters/CSi/Speckle.Converters.ETABSShared/ToSpeckle/Helpers/EtabsJointPropertiesExtractor.cs b/Converters/CSi/Speckle.Converters.ETABSShared/ToSpeckle/Helpers/EtabsJointPropertiesExtractor.cs index 9709ae653..593145f39 100644 --- a/Converters/CSi/Speckle.Converters.ETABSShared/ToSpeckle/Helpers/EtabsJointPropertiesExtractor.cs +++ b/Converters/CSi/Speckle.Converters.ETABSShared/ToSpeckle/Helpers/EtabsJointPropertiesExtractor.cs @@ -33,10 +33,10 @@ public EtabsJointPropertiesExtractor(IConverterSettingsStore properties) { - var objectId = DictionaryUtils.EnsureNestedDictionary(properties, "Object ID"); + var objectId = DictionaryUtils.EnsureNestedDictionary(properties, ObjectPropertyCategory.OBJECT_ID); (objectId["label"], objectId["level"]) = GetLabelAndLevel(joint); - var assignments = DictionaryUtils.EnsureNestedDictionary(properties, "Assignments"); + var assignments = DictionaryUtils.EnsureNestedDictionary(properties, ObjectPropertyCategory.ASSIGNMENTS); (assignments["diaphragmOption"], assignments["diaphragmName"]) = GetAssignedDiaphragm(joint); assignments["springAssignment"] = GetSpringAssignmentName(joint); } diff --git a/Converters/CSi/Speckle.Converters.ETABSShared/ToSpeckle/Helpers/EtabsShellPropertiesExtractor.cs b/Converters/CSi/Speckle.Converters.ETABSShared/ToSpeckle/Helpers/EtabsShellPropertiesExtractor.cs index 638b3fb7c..e4c126b2b 100644 --- a/Converters/CSi/Speckle.Converters.ETABSShared/ToSpeckle/Helpers/EtabsShellPropertiesExtractor.cs +++ b/Converters/CSi/Speckle.Converters.ETABSShared/ToSpeckle/Helpers/EtabsShellPropertiesExtractor.cs @@ -1,5 +1,7 @@ +using System.Collections.Concurrent; using Speckle.Converters.Common; using Speckle.Converters.CSiShared; +using Speckle.Converters.CSiShared.Extensions; using Speckle.Converters.CSiShared.ToSpeckle.Helpers; using Speckle.Converters.CSiShared.Utils; @@ -25,24 +27,69 @@ namespace Speckle.Converters.ETABSShared.ToSpeckle.Helpers; public sealed class EtabsShellPropertiesExtractor { private readonly IConverterSettingsStore _settingsStore; + private readonly MaterialNameLookup _materialNameLookup; + private readonly CsiToSpeckleCacheSingleton _csiToSpeckleCacheSingleton; - public EtabsShellPropertiesExtractor(IConverterSettingsStore settingsStore) + public EtabsShellPropertiesExtractor( + CsiToSpeckleCacheSingleton csiToSpeckleCacheSingleton, + IConverterSettingsStore settingsStore + ) { _settingsStore = settingsStore; + _materialNameLookup = new MaterialNameLookup(settingsStore); + _csiToSpeckleCacheSingleton = csiToSpeckleCacheSingleton; } public void ExtractProperties(CsiShellWrapper shell, Dictionary properties) { - var objectId = DictionaryUtils.EnsureNestedDictionary(properties, "Object ID"); + var objectId = DictionaryUtils.EnsureNestedDictionary(properties, ObjectPropertyCategory.OBJECT_ID); objectId["designOrientation"] = GetDesignOrientation(shell); (objectId["label"], objectId["level"]) = GetLabelAndLevel(shell); - var assignments = DictionaryUtils.EnsureNestedDictionary(properties, "Assignments"); + var assignments = DictionaryUtils.EnsureNestedDictionary(properties, ObjectPropertyCategory.ASSIGNMENTS); assignments["diaphragmName"] = GetAssignedDiaphragmName(shell); assignments["isOpening"] = IsOpening(shell); assignments["pierAssignment"] = GetPierAssignmentName(shell); assignments["spandrelAssignment"] = GetSpandrelAssignmentName(shell); assignments["springAssignmentName"] = GetSpringAssignmentName(shell); + + // NOTE: sectionId and materialId a "quick-fix" to enable filtering in the viewer etc. + // Assign sectionId to variable as this will be an argument for the GetMaterialName method + string shellAppId = shell.GetSpeckleApplicationId(_settingsStore.Current.SapModel); + string sectionId = GetSectionName(shell); + string materialId = _materialNameLookup.GetMaterialForSection(sectionId); + assignments[ObjectPropertyKey.SECTION_ID] = sectionId; + assignments[ObjectPropertyKey.MATERIAL_ID] = materialId; + + // store the object, section, and material id relationships in their corresponding caches to be accessed by the connector + if (!string.IsNullOrEmpty(sectionId)) + { + if (_csiToSpeckleCacheSingleton.ShellSectionCache.TryGetValue(sectionId, out List? shellIds)) + { + shellIds.Add(shellAppId); + } + else + { + _csiToSpeckleCacheSingleton.ShellSectionCache.Add(sectionId, new List() { shellAppId }); + } + + if (!string.IsNullOrEmpty(materialId)) + { + if (_csiToSpeckleCacheSingleton.MaterialCache.TryGetValue(materialId, out List? sectionIds)) + { + // Since this is happening on the object level, we could be processing the same sectionIds (from different + // objects) many times. This is not necessary since we just want a set of sectionId corresponding to material + if (!sectionIds.Contains(sectionId)) + { + sectionIds.Add(sectionId); + } + } + else + { + _csiToSpeckleCacheSingleton.MaterialCache.Add(materialId, new List() { sectionId }); + } + } + } } private (string label, string level) GetLabelAndLevel(CsiShellWrapper shell) @@ -90,8 +137,85 @@ private string GetSpandrelAssignmentName(CsiShellWrapper shell) private string GetSpringAssignmentName(CsiShellWrapper shell) { - string springAssignmentName = "None"; // Is there a better way to handle null? + string springAssignmentName = string.Empty; _ = _settingsStore.Current.SapModel.AreaObj.GetSpringAssignment(shell.Name, ref springAssignmentName); return springAssignmentName; } + + // NOTE: Moved from CsiShellPropertiesExtractor because of the materialId issue. + // Results of the cDatabaseTable query for "Area Section Property Definitions - Summary" vary between Sap and Etabs + private string GetSectionName(CsiShellWrapper shell) + { + string sectionName = string.Empty; + _ = _settingsStore.Current.SapModel.AreaObj.GetProperty(shell.Name, ref sectionName); + return sectionName; + } + + // TODO: This is a temporary solution until proper DatabaseTables implementation is available. + // FrameObj can use the following query: PropFrame.GetMaterial + // AreaObj doesn't have a PropArea.GetMaterial method + // So, what to do? Simplest solution: query the cDatabaseTable for the summary of area sections + // Cache the results as a dictionary where keys are sectionName and values are materialId + // Use the cached result to return the material string given a section name + // This is a temporary solution! The use of cDatabaseTable are being explored as a way to simplify a lot moving forward + private sealed class MaterialNameLookup + { + private readonly IConverterSettingsStore _settingsStore; + private readonly ConcurrentDictionary _materialLookup = new(); + private bool _isInitialized; + + public MaterialNameLookup(IConverterSettingsStore settingsStore) + { + _settingsStore = settingsStore; + } + + public string GetMaterialForSection(string sectionName) + { + if (!_isInitialized) + { + InitializeCache(); + } + + return _materialLookup.TryGetValue(sectionName, out string? value) ? value : string.Empty; + } + + private void InitializeCache() + { + string[] fieldKeyList = [], + fieldKeysIncluded = [], + tableData = []; + int tableVersion = 0, + numberOfRecords = 0; + + int result = _settingsStore.Current.SapModel.DatabaseTables.GetTableForDisplayArray( + "Area Section Property Definitions - Summary", + ref fieldKeyList, + "", + ref tableVersion, + ref fieldKeysIncluded, + ref numberOfRecords, + ref tableData + ); + + if (result != 0 || numberOfRecords == 0) + { + _isInitialized = true; // Mark as initialized even on failure + return; + } + + // Process each record (each record has fieldKeysIncluded.Length columns) + for (int i = 0; i < tableData.Length; i += fieldKeysIncluded.Length) + { + string name = tableData[i]; // Name is first column + string material = tableData[i + 3]; // Material is fourth column + + if (!string.IsNullOrEmpty(name)) + { + _materialLookup.TryAdd(name, material); + } + } + + _isInitialized = true; + } + } } diff --git a/Sdk/Speckle.Connectors.Common/Operations/ProxyKeys.cs b/Sdk/Speckle.Connectors.Common/Operations/ProxyKeys.cs index a0abff694..d82612751 100644 --- a/Sdk/Speckle.Connectors.Common/Operations/ProxyKeys.cs +++ b/Sdk/Speckle.Connectors.Common/Operations/ProxyKeys.cs @@ -8,4 +8,6 @@ public static class ProxyKeys public const string GROUP = "groupProxies"; public const string PARAMETER_DEFINITIONS = "parameterDefinitions"; public const string PROPERTYSET_DEFINITIONS = "propertySetDefinitions"; + public const string MATERIAL = "materialProxies"; + public const string SECTION = "sectionProxies"; }