From 68f7cb8f418251ca6df9176ad839b5846f8378e9 Mon Sep 17 00:00:00 2001 From: Claire Kuang Date: Thu, 24 Oct 2024 14:04:20 +0100 Subject: [PATCH] fix(civil3d): refactors corridors to only use extracted solids once (#318) * refactors corridors to only use extracted solids once changes applied assemblies and subassemblies to dicts * Update CorridorHandler.cs * refactors corridor display value extractor to its own class * changes scope --- .../Helpers/CorridorDisplayValueExtractor.cs | 184 +++++++++++ .../Helpers/CorridorHandler.cs | 285 +++++++----------- .../Helpers/DisplayValueExtractor.cs | 4 + .../ServiceRegistration.cs | 1 + ...Speckle.Converters.Civil3dShared.projitems | 1 + 5 files changed, 292 insertions(+), 183 deletions(-) create mode 100644 Converters/Civil3d/Speckle.Converters.Civil3dShared/Helpers/CorridorDisplayValueExtractor.cs diff --git a/Converters/Civil3d/Speckle.Converters.Civil3dShared/Helpers/CorridorDisplayValueExtractor.cs b/Converters/Civil3d/Speckle.Converters.Civil3dShared/Helpers/CorridorDisplayValueExtractor.cs new file mode 100644 index 000000000..8f49b1017 --- /dev/null +++ b/Converters/Civil3d/Speckle.Converters.Civil3dShared/Helpers/CorridorDisplayValueExtractor.cs @@ -0,0 +1,184 @@ +using Speckle.Converters.Common; +using Speckle.Converters.Common.Objects; +using Speckle.Sdk; + +namespace Speckle.Converters.Civil3dShared.Helpers; + +/// +/// Constructs a the Corridor Key for a subassembly with the Corridor Handle, Baseline Guid, Region Guid, Assembly Handle, and Subassembly Handle. +/// This order and type is determined by the structure of corridors and the available information on extracted corridor solid property sets. +/// +public readonly struct SubassemblyCorridorKey +{ + public string CorridorId { get; } + public string BaselineId { get; } + public string RegionId { get; } + public string AssemblyId { get; } + public string SubassemblyId { get; } + + public SubassemblyCorridorKey( + string corridorHandle, + string baselineGuid, + string regionGuid, + string assemblyHandle, + string subassemblyHandle + ) + { + CorridorId = corridorHandle.ToLower(); + BaselineId = CleanGuidString(baselineGuid.ToLower()); + RegionId = CleanGuidString(regionGuid.ToLower()); + AssemblyId = assemblyHandle.ToLower(); + SubassemblyId = subassemblyHandle.ToLower(); + } + + // Removes brackets from guid strings - property sets will return guids with brackets (unlike when retrieved from api) + private string CleanGuidString(string guid) + { + guid = guid.Replace("{", "").Replace("}", ""); + return guid; + } + + public override readonly string ToString() => $"{CorridorId}-{BaselineId}-{RegionId}-{AssemblyId}-{SubassemblyId}"; +} + +/// +/// Extracts and stores the display value meshes of a Corridor by the corridor's subassembly corridor keys. +/// +public sealed class CorridorDisplayValueExtractor +{ + /// + /// Keeps track of all corridor solids by their hierarchy of (corridor, baseline, region, applied assembly, applied subassembly) in the current send operation. + /// This should be added to the display value of the corridor applied subassemblies after they are processed + /// Handles should be used instead of Handle.Value (as is typically used for speckle app ids) since the exported solid property sets only stores the handle + /// + public Dictionary> CorridorSolidsCache { get; } = new(); + + // these ints are used to retrieve the correct values from the exported corridor solids property sets to cache them + // they were determined via trial and error +#pragma warning disable CA1805 // Initialized explicitly to 0 + private readonly int _corridorHandleIndex = 0; +#pragma warning restore CA1805 // Initialized explicitly to 0 + private readonly int _baselineGuidIndex = 6; + private readonly int _regionGuidIndex = 7; + private readonly int _assemblyHandleIndex = 3; + private readonly int _subassemblyHandleIndex = 4; + + private readonly ITypedConverter _solidConverter; + private readonly ITypedConverter _bodyConverter; + private readonly IConverterSettingsStore _settingsStore; + + public CorridorDisplayValueExtractor( + ITypedConverter solidConverter, + ITypedConverter bodyConverter, + IConverterSettingsStore settingsStore + ) + { + _solidConverter = solidConverter; + _bodyConverter = bodyConverter; + _settingsStore = settingsStore; + } + + /// + /// Extracts the solids from a corridor and stores them in by their subassembly corridor id. + /// Api method is only available for 2024 or greater. + /// + /// + /// This is pretty complicated because we need to match each extracted solid to a corridor subassembly by inspecting its property sets for identifying information + public void ProcessCorridorSolids(CDB.Corridor corridor) + { +#if CIVIL3D2024_OR_GREATER + + CDB.ExportCorridorSolidsParams param = new(); + + using (var tr = _settingsStore.Current.Document.Database.TransactionManager.StartTransaction()) + { + foreach (ADB.ObjectId solidId in corridor.ExportSolids(param, corridor.Database)) + { + SOG.Mesh? mesh = null; + var solid = tr.GetObject(solidId, ADB.OpenMode.ForRead); + if (solid is ADB.Solid3d solid3d) + { + // get the solid mesh + mesh = _solidConverter.Convert(solid3d); + } + else if (solid is ADB.Body body) + { + mesh = _bodyConverter.Convert(body); + } + + if (mesh is null) + { + continue; + } + + // get the subassembly corridor key from the solid property sets + if (GetSubassemblyKeyFromDBObject(solid, tr) is SubassemblyCorridorKey solidKey) + { + if (CorridorSolidsCache.TryGetValue(solidKey.ToString(), out List? display)) + { + display.Add(mesh); + } + else + { + CorridorSolidsCache[solidKey.ToString()] = new() { mesh }; + } + } + } + + tr.Commit(); + } +#endif + } + + private SubassemblyCorridorKey? GetSubassemblyKeyFromDBObject(ADB.DBObject obj, ADB.Transaction tr) + { + ADB.ObjectIdCollection? propertySetIds; + + try + { + propertySetIds = AAECPDB.PropertyDataServices.GetPropertySets(obj); + } + catch (Exception e) when (!e.IsFatal()) + { + return null; + } + + if (propertySetIds is null || propertySetIds.Count == 0) + { + return null; + } + + foreach (ADB.ObjectId id in propertySetIds) + { + AAECPDB.PropertySet propertySet = (AAECPDB.PropertySet)tr.GetObject(id, ADB.OpenMode.ForRead); + + if (propertySet.PropertySetDefinitionName == "Corridor Identity") + { + if (propertySet.PropertySetData[_corridorHandleIndex].GetData() is not string corridorHandle) + { + return null; + } + if (propertySet.PropertySetData[_baselineGuidIndex].GetData() is not string baselineGuid) + { + return null; + } + if (propertySet.PropertySetData[_regionGuidIndex].GetData() is not string regionGuid) + { + return null; + } + if (propertySet.PropertySetData[_assemblyHandleIndex].GetData() is not string assemblyHandle) + { + return null; + } + if (propertySet.PropertySetData[_subassemblyHandleIndex].GetData() is not string subassemblyHandle) + { + return null; + } + + return new SubassemblyCorridorKey(corridorHandle, baselineGuid, regionGuid, assemblyHandle, subassemblyHandle); + } + } + + return null; + } +} diff --git a/Converters/Civil3d/Speckle.Converters.Civil3dShared/Helpers/CorridorHandler.cs b/Converters/Civil3d/Speckle.Converters.Civil3dShared/Helpers/CorridorHandler.cs index 6cc4ea652..0e35e13ce 100644 --- a/Converters/Civil3d/Speckle.Converters.Civil3dShared/Helpers/CorridorHandler.cs +++ b/Converters/Civil3d/Speckle.Converters.Civil3dShared/Helpers/CorridorHandler.cs @@ -6,51 +6,37 @@ namespace Speckle.Converters.Civil3dShared.Helpers; +/// +/// Processes the children of a corridor. Expects to be a singleton service. +/// public sealed class CorridorHandler { - /// - /// Keeps track of all corridor solids by their hierarchy of (corridor, baseline, region, applied assembly, applied subassembly) in the current send operation. - /// This should be added to the display value of the corridor applied subassemblies after they are processed - /// Handles should be used instead of Handle.Value (as is typically used for speckle app ids) since the exported solid property sets only stores the handle - /// - public Dictionary<(string, string, string, string, string), List> CorridorSolidsCache { get; } = new(); - - // these ints are used to retrieve the correct values from the exported corridor solids property sets to cache them - // they were determined via trial and error -#pragma warning disable CA1805 // Initialized explicitly to 0 - private readonly int _corridorHandleIndex = 0; -#pragma warning restore CA1805 // Initialized explicitly to 0 - private readonly int _baselineGuidIndex = 6; - private readonly int _regionGuidIndex = 7; - private readonly int _assemblyHandleIndex = 3; - private readonly int _subassemblyHandleIndex = 4; - - private readonly ITypedConverter _solidConverter; - private readonly ITypedConverter _bodyConverter; + private readonly ITypedConverter _pointConverter; private readonly ITypedConverter _pointCollectionConverter; + private readonly CorridorDisplayValueExtractor _displayValueExtractor; private readonly IConverterSettingsStore _settingsStore; public CorridorHandler( - ITypedConverter solidConverter, - ITypedConverter bodyConverter, + ITypedConverter pointConverter, ITypedConverter pointCollectionConverter, + CorridorDisplayValueExtractor displayValueExtractor, IConverterSettingsStore settingsStore ) { - _solidConverter = solidConverter; - _bodyConverter = bodyConverter; + _pointConverter = pointConverter; _pointCollectionConverter = pointCollectionConverter; + _displayValueExtractor = displayValueExtractor; _settingsStore = settingsStore; } // Ok, this is going to be very complicated. - // We are building a nested `Base.elements` of corridor subelements in this hierarchy: corridor -> baselines -> baseline regions -> applied assemblies -> applied subassemblies - // This is because none of these entities inherit from CDB.Entity, and we need to match the corridor solids with the corresponding applied subassembly. + // We are building a nested `Base.elements` of corridor subelements in this hierarchy: corridor -> baselines -> baseline regions -> assembly -> subassemblies. + // Corridors will also have a dict of applied assemblies -> applied subassemblies attached to the region. + // This handler is in place because none of the corridor children inherit from CDB.Entity public List GetCorridorChildren(CDB.Corridor corridor) { - // first extract all corridor solids. - // this needs to be done before traversing children, so we can match the solid mesh to the appropriate subassembly - HandleCorridorSolids(corridor); + // extract corridor solids for display value first: this will be used later to attach display values to subassemblies. + _displayValueExtractor.ProcessCorridorSolids(corridor); // track children hierarchy ids: string corridorHandle = corridor.Handle.ToString(); @@ -138,32 +124,85 @@ CDB.FeatureLineCollection featurelineCollection in offsetFeaturelineCollection.F ["name"] = region.Name, ["startStation"] = region.StartStation, ["endStation"] = region.EndStation, - ["assemblyId"] = region.AssemblyId.GetSpeckleApplicationId(), ["units"] = _settingsStore.Current.SpeckleUnits, ["applicationId"] = regionGuid, }; - // get the region applied assemblies - List appliedAssemblies = new(); + // traverse region assembly for subassemblies and codes + // display values (corridor solids) will be dumped here, by their code + Dictionary subassemblyNameCache = new(); + using (var tr = _settingsStore.Current.Document.Database.TransactionManager.StartTransaction()) + { + var assembly = (CDB.Assembly)tr.GetObject(region.AssemblyId, ADB.OpenMode.ForRead); + string assemblyHandle = region.AssemblyId.Handle.ToString(); + + // traverse groups for subassemblies + List subassemblies = new(); + foreach (CDB.AssemblyGroup group in assembly.Groups) + { + foreach (ADB.ObjectId subassemblyId in group.GetSubassemblyIds()) + { + var subassembly = (CDB.Subassembly)tr.GetObject(subassemblyId, ADB.OpenMode.ForRead); + string subassemblyHandle = subassemblyId.Handle.ToString(); + + // store name in cache for later use by applied subassemblies + subassemblyNameCache[subassemblyId] = subassembly.Name; + + Base convertedSubassembly = + new() + { + ["name"] = subassembly.Name, + ["type"] = subassembly.GetType().ToString().Split('.').Last(), + applicationId = subassembly.GetSpeckleApplicationId() + }; + + // try to get the display value mesh from the corridor display value extractor by subassembly key + SubassemblyCorridorKey subassemblyKey = + new(corridorHandle, baselineGuid, regionGuid, assemblyHandle, subassemblyHandle); + + if ( + _displayValueExtractor.CorridorSolidsCache.TryGetValue( + subassemblyKey.ToString(), + out List? display + ) + ) + { + convertedSubassembly["displayValue"] = display; + } + + subassemblies.Add(convertedSubassembly); + } + } + + Base convertedAssembly = + new() + { + ["name"] = assembly.Name, + ["type"] = assembly.GetType().ToString().Split('.').Last(), + ["subassemblies"] = subassemblies, + applicationId = assembly.GetSpeckleApplicationId() + }; + + convertedRegion["assembly"] = convertedAssembly; + + tr.Commit(); + } + + // now get all region applied assemblies, applied subassemblies, and calculated shapes, links, and points as dicts + Dictionary appliedAssemblies = new(); double[] sortedStations = region.SortedStations(); for (int i = 0; i < sortedStations.Length; i++) { double station = sortedStations[i]; CDB.AppliedAssembly appliedAssembly = region.AppliedAssemblies[i]; - string assemblyHandle = appliedAssembly.AssemblyId.Handle.ToString(); - Base convertedAppliedAssembly = - new() - { - ["type"] = appliedAssembly.GetType().ToString().Split('.').Last(), - ["assemblyId"] = appliedAssembly.AssemblyId.GetSpeckleApplicationId(), - ["station"] = station, - ["units"] = _settingsStore.Current.SpeckleUnits - }; + + Dictionary appliedAssemblyDict = + new() { ["assemblyId"] = appliedAssembly.AssemblyId.GetSpeckleApplicationId(), ["station"] = station }; try { - convertedAppliedAssembly["adjustedElevation"] = appliedAssembly.AdjustedElevation; + appliedAssemblyDict["adjustedElevation"] = appliedAssembly.AdjustedElevation; } catch (ArgumentException e) when (!e.IsFatal()) { @@ -171,45 +210,29 @@ CDB.FeatureLineCollection featurelineCollection in offsetFeaturelineCollection.F } // get the applied assembly's applied subassemblies - List appliedSubassemblies = new(appliedAssembly.GetAppliedSubassemblies().Count); - + Dictionary appliedSubassemblies = new(); foreach (CDB.AppliedSubassembly appliedSubassembly in appliedAssembly.GetAppliedSubassemblies()) { - string subassemblyHandle = appliedSubassembly.SubassemblyId.Handle.ToString(); + string subassemblyId = appliedSubassembly.SubassemblyId.GetSpeckleApplicationId(); + string name = subassemblyNameCache.TryGetValue(appliedSubassembly.SubassemblyId, out string? cachedName) + ? cachedName + : subassemblyId; - Base convertedAppliedSubassembly = + Dictionary appliedSubassemblyDict = new() { - ["type"] = appliedSubassembly.GetType().ToString().Split('.').Last(), - ["subassemblyId"] = appliedSubassembly.SubassemblyId.GetSpeckleApplicationId(), - ["units"] = _settingsStore.Current.SpeckleUnits + ["subassemblyId"] = subassemblyId, + ["calculatedShapes"] = GetCalculatedShapes(appliedSubassembly) }; - // try to get the display value mesh - (string, string, string, string, string) corridorSolidsKey = ( - corridorHandle, - baselineGuid, - regionGuid, - assemblyHandle, - subassemblyHandle - ); - - if (CorridorSolidsCache.TryGetValue(corridorSolidsKey, out List? display)) - { - convertedAppliedSubassembly["displayValue"] = display; - } - - // get the applied subassembly's calculated stuff - AddSubassemblyCalculatedProperties(convertedAppliedSubassembly, appliedSubassembly); - - appliedSubassemblies.Add(convertedAppliedSubassembly); + appliedSubassemblies[name] = appliedSubassemblyDict; } + appliedAssemblyDict["appliedSubassemblies"] = appliedSubassemblies; - convertedAppliedAssembly["elements"] = appliedSubassemblies; - appliedAssemblies.Add(convertedAppliedAssembly); + appliedAssemblies[station.ToString()] = appliedAssemblyDict; } - convertedRegion["elements"] = appliedAssemblies; + convertedRegion["appliedAssemblies"] = appliedAssemblies; regions.Add(convertedRegion); } @@ -220,11 +243,8 @@ CDB.FeatureLineCollection featurelineCollection in offsetFeaturelineCollection.F return baselines; } - // Adds the calculated shapes > calculated links > calculated points as dicts to the applied subassembly - private void AddSubassemblyCalculatedProperties( - Base speckleAppliedSubassembly, - CDB.AppliedSubassembly appliedSubassembly - ) + // Gets the calculated shapes > calculated links > calculated points of an applied subassembly + private Dictionary GetCalculatedShapes(CDB.AppliedSubassembly appliedSubassembly) { Dictionary calculatedShapes = new(); int shapeCount = 0; @@ -240,7 +260,7 @@ CDB.AppliedSubassembly appliedSubassembly { calculatedPoints[pointCount.ToString()] = new Dictionary() { - ["xyz"] = point.XYZ.ToArray(), + ["xyz"] = _pointConverter.Convert(point.XYZ), ["corridorCodes"] = point.CorridorCodes.ToList(), ["stationOffsetElevationToBaseline"] = point.StationOffsetElevationToBaseline.ToArray(), }; @@ -263,8 +283,7 @@ CDB.AppliedSubassembly appliedSubassembly ["calculatedLinks"] = calculatedLinks }; } - - speckleAppliedSubassembly["calculatedShapes"] = calculatedShapes; + return calculatedShapes; } private Base FeatureLineToSpeckle(CDB.CorridorFeatureLine featureline) @@ -288,112 +307,12 @@ private Base FeatureLineToSpeckle(CDB.CorridorFeatureLine featureline) } // create featureline - return new() { ["codeName"] = featureline.CodeName, ["displayValue"] = polylines }; - } - - /// - /// Extracts the solids from a corridor and stores in according to property sets on the solid. - /// NOTE: The Export Solids method is only available for version 2024 or greater - /// - /// - /// - private void HandleCorridorSolids(CDB.Corridor corridor) - { -#if CIVIL3D2024_OR_GREATER - CDB.ExportCorridorSolidsParams param = new(); - - using (var tr = _settingsStore.Current.Document.Database.TransactionManager.StartTransaction()) - { - foreach (ADB.ObjectId solidId in corridor.ExportSolids(param, corridor.Database)) - { - SOG.Mesh? mesh = null; - var solid = tr.GetObject(solidId, ADB.OpenMode.ForRead); - if (solid is ADB.Solid3d solid3d) - { - // get the solid mesh - mesh = _solidConverter.Convert(solid3d); - } - else if (solid is ADB.Body body) - { - mesh = _bodyConverter.Convert(body); - } - - if (mesh is null) - { - continue; - } - - // get the (corridor handle, baseline guid, region guid, assembly handle, subassembly handle) of the solid property sets - (string corridor, string baseline, string region, string assembly, string subassembly)? solidKey = - GetCorridorSolidIdFromPropertySet(solid, tr); - - if (solidKey is (string, string, string, string, string) validSolidKey) - { - if (CorridorSolidsCache.TryGetValue(validSolidKey, out List? display)) - { - display.Add(mesh); - } - else - { - CorridorSolidsCache[validSolidKey] = new() { mesh }; - } - } - } - - tr.Commit(); - } -#endif - } - - private (string, string, string, string, string)? GetCorridorSolidIdFromPropertySet( - ADB.DBObject obj, - ADB.Transaction tr - ) - { - ADB.ObjectIdCollection? propertySetIds; - - try + return new() { - propertySetIds = AAECPDB.PropertyDataServices.GetPropertySets(obj); - } - catch (Exception e) when (!e.IsFatal()) - { - return null; - } - - if (propertySetIds is null || propertySetIds.Count == 0) - { - return null; - } - - foreach (ADB.ObjectId id in propertySetIds) - { - AAECPDB.PropertySet propertySet = (AAECPDB.PropertySet)tr.GetObject(id, ADB.OpenMode.ForRead); - - if (propertySet.PropertySetDefinitionName == "Corridor Identity") - { - AAECPDB.PropertySetData corridorData = propertySet.PropertySetData[_corridorHandleIndex]; - AAECPDB.PropertySetData baselineData = propertySet.PropertySetData[_baselineGuidIndex]; - AAECPDB.PropertySetData regionData = propertySet.PropertySetData[_regionGuidIndex]; - AAECPDB.PropertySetData assemblyData = propertySet.PropertySetData[_assemblyHandleIndex]; - AAECPDB.PropertySetData subassemblyData = propertySet.PropertySetData[_subassemblyHandleIndex]; - - return - corridorData.GetData() is not string corridorHandle - || baselineData.GetData() is not string baselineGuid // guid is uppercase and enclosed in {} which need to be removed - || regionData.GetData() is not string regionGuid // guid is uppercase and enclosed in {} which need to be removed - || assemblyData.GetData() is not string assemblyHandle - || subassemblyData.GetData() is not string subassemblyHandle - ? null - : ( - corridorHandle, - baselineGuid[1..^1].ToLower(), - regionGuid[1..^1].ToLower(), - assemblyHandle, - subassemblyHandle - ); - } - } - return null; + ["name"] = featureline.CodeName, + ["type"] = featureline.GetType().ToString().Split('.').Last(), + ["codeName"] = featureline.CodeName, + ["displayValue"] = polylines + }; } } diff --git a/Converters/Civil3d/Speckle.Converters.Civil3dShared/Helpers/DisplayValueExtractor.cs b/Converters/Civil3d/Speckle.Converters.Civil3dShared/Helpers/DisplayValueExtractor.cs index d85d81c8e..af1c6ae2d 100644 --- a/Converters/Civil3d/Speckle.Converters.Civil3dShared/Helpers/DisplayValueExtractor.cs +++ b/Converters/Civil3d/Speckle.Converters.Civil3dShared/Helpers/DisplayValueExtractor.cs @@ -55,6 +55,10 @@ IConverterSettingsStore converterSettings SOG.Mesh gridSurfaceMesh = _gridSurfaceConverter.Convert(gridSurface); return new() { gridSurfaceMesh }; + // Corridors are complicated: their display values are extracted in the CorridorHandler when processing corridor children, since they are attached to the corridor subassemblies. + case CDB.Corridor: + return new(); + default: return null; } diff --git a/Converters/Civil3d/Speckle.Converters.Civil3dShared/ServiceRegistration.cs b/Converters/Civil3d/Speckle.Converters.Civil3dShared/ServiceRegistration.cs index 38f277c47..6b57c26a0 100644 --- a/Converters/Civil3d/Speckle.Converters.Civil3dShared/ServiceRegistration.cs +++ b/Converters/Civil3d/Speckle.Converters.Civil3dShared/ServiceRegistration.cs @@ -45,5 +45,6 @@ public static void AddCivil3dConverters(this IServiceCollection serviceCollectio serviceCollection.AddScoped(); serviceCollection.AddScoped(); serviceCollection.AddScoped(); + serviceCollection.AddScoped(); } } diff --git a/Converters/Civil3d/Speckle.Converters.Civil3dShared/Speckle.Converters.Civil3dShared.projitems b/Converters/Civil3d/Speckle.Converters.Civil3dShared/Speckle.Converters.Civil3dShared.projitems index 62c0f54d4..db39ca9d0 100644 --- a/Converters/Civil3d/Speckle.Converters.Civil3dShared/Speckle.Converters.Civil3dShared.projitems +++ b/Converters/Civil3d/Speckle.Converters.Civil3dShared/Speckle.Converters.Civil3dShared.projitems @@ -15,6 +15,7 @@ +