Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bjorn/cnx-835-add-converter-projects-and-top-level-converter #429

Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Speckle.Connectors.CSiShared.HostApp;
using Speckle.Connectors.CSiShared.Utils;
using Speckle.Connectors.DUI.Bindings;
using Speckle.Connectors.DUI.Bridge;

Expand All @@ -8,17 +9,23 @@ public class CSiSharedSelectionBinding : ISelectionBinding
{
public string Name => "selectionBinding";
public IBrowserBridge Parent { get; }
private readonly ICSiApplicationService _csiApplicationService; // Update selection binding to centralized CSiSharedApplicationService instead of trying to maintain a reference to "sapModel"
private readonly ICSiApplicationService _csiApplicationService;

public CSiSharedSelectionBinding(IBrowserBridge parent, ICSiApplicationService csiApplicationService)
{
Parent = parent;
_csiApplicationService = csiApplicationService;
}

/// <summary>
/// Gets the selection and creates an encoded ID (objectType and objectName).
/// </summary>
/// <remarks>
/// Refer to ObjectIdentifier.cs for more info.
/// </remarks>
public SelectionInfo GetSelection()
{
// TODO: Handle better. Enums? ObjectType same in ETABS and SAP
// TODO: Since this is standard across CSi Suite - better stored in an enum?
var objectTypeMap = new Dictionary<int, string>
{
{ 1, "Point" },
Expand All @@ -44,8 +51,8 @@ public SelectionInfo GetSelection()
var typeKey = objectType[i];
var typeName = objectTypeMap.TryGetValue(typeKey, out var name) ? name : $"Unknown ({typeKey})";

encodedIds.Add(EncodeObjectIdentifier(typeKey, objectName[i]));
typeCounts[typeName] = (typeCounts.TryGetValue(typeName, out var count) ? count : 0) + 1; // NOTE: Cross-framework compatibility
encodedIds.Add(ObjectIdentifier.Encode(typeKey, objectName[i]));
typeCounts[typeName] = (typeCounts.TryGetValue(typeName, out var count) ? count : 0) + 1; // NOTE: Cross-framework compatibility (net 48 and net8)
}

var summary =
Expand All @@ -56,21 +63,4 @@ public SelectionInfo GetSelection()

return new SelectionInfo(encodedIds, summary);
}

// NOTE: All API methods are based on the objectType and objectName, not the GUID
// We will obviously manage the GUIDs but for all method calls we need a concatenated version of the objectType and objectName
// Since objectType >= 1 and <= 7, we know first index will always be the objectType
// Remaining string represents objectName and since the user can add any string (provided it is unique), this is safer
// than using a delimiting character (which could clash with user string)
private string EncodeObjectIdentifier(int objectType, string objectName)
{
// Just in case some weird objectType pops up
if (objectType < 1 || objectType > 7)
{
throw new ArgumentException($"Invalid object type: {objectType}. Must be between 1 and 7.");
}

// Simply prepend the object type as a single character
return $"{objectType}{objectName}";
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Speckle.Connectors.Common.Cancellation;
using Speckle.Connectors.Common.Operations;
using Speckle.Connectors.CSiShared.HostApp;
using Speckle.Connectors.CSiShared.Utils;
using Speckle.Connectors.DUI.Bindings;
using Speckle.Connectors.DUI.Bridge;
using Speckle.Connectors.DUI.Exceptions;
using Speckle.Connectors.DUI.Logging;
using Speckle.Connectors.DUI.Models;
using Speckle.Connectors.DUI.Models.Card;
using Speckle.Connectors.DUI.Models.Card.SendFilter;
using Speckle.Connectors.DUI.Settings;
using Speckle.Converters.Common;
using Speckle.Converters.CSiShared;
using Speckle.Sdk;
using Speckle.Sdk.Common;
using Speckle.Sdk.Logging;

namespace Speckle.Connectors.CSiShared.Bindings;

Expand All @@ -21,6 +33,10 @@ public sealed class CSiSharedSendBinding : ISendBinding
private readonly CancellationManager _cancellationManager;
private readonly IOperationProgressManager _operationProgressManager;
private readonly ILogger<CSiSharedSendBinding> _logger;
private readonly ICSiApplicationService _csiApplicationService;
private readonly ICSiConversionSettingsFactory _csiConversionSettingsFactory;
private readonly ISpeckleApplication _speckleApplication;
private readonly ISdkActivityFactory _activityFactory;

public CSiSharedSendBinding(
DocumentModelStore store,
Expand All @@ -30,7 +46,11 @@ public CSiSharedSendBinding(
IServiceProvider serviceProvider,
CancellationManager cancellationManager,
IOperationProgressManager operationProgressManager,
ILogger<CSiSharedSendBinding> logger
ILogger<CSiSharedSendBinding> logger,
ICSiConversionSettingsFactory csiConversionSettingsFactory,
ISpeckleApplication speckleApplication,
ISdkActivityFactory activityFactory,
ICSiApplicationService csiApplicationService
)
{
_store = store;
Expand All @@ -42,6 +62,10 @@ ILogger<CSiSharedSendBinding> logger
_logger = logger;
Parent = parent;
Commands = new SendBindingUICommands(parent);
_csiConversionSettingsFactory = csiConversionSettingsFactory;
_speckleApplication = speckleApplication;
_activityFactory = activityFactory;
_csiApplicationService = csiApplicationService;
}

public List<ISendFilter> GetSendFilters() => _sendFilters;
Expand All @@ -50,8 +74,61 @@ ILogger<CSiSharedSendBinding> logger

public async Task Send(string modelCardId)
{
// placeholder for actual send implementation
await Task.CompletedTask.ConfigureAwait(false);
using var activity = _activityFactory.Start();

try
{
if (_store.GetModelById(modelCardId) is not SenderModelCard modelCard)
{
throw new InvalidOperationException("No publish model card was found.");
}
using var scope = _serviceProvider.CreateScope();
scope
.ServiceProvider.GetRequiredService<IConverterSettingsStore<CSiConversionSettings>>()
.Initialize(_csiConversionSettingsFactory.Create(_csiApplicationService.SapModel));

CancellationToken cancellationToken = _cancellationManager.InitCancellationTokenSource(modelCardId);

List<ICSiWrapper> wrappers = modelCard
.SendFilter.NotNull()
.RefreshObjectIds()
.Select(DecodeObjectIdentifier)
.ToList();

if (wrappers.Count == 0)
{
throw new SpeckleSendFilterException("No objects were found to convert. Please update your publish filter!");
}

var sendResult = await scope
.ServiceProvider.GetRequiredService<SendOperation<ICSiWrapper>>()
.Execute(
wrappers,
modelCard.GetSendInfo(_speckleApplication.Slug),
_operationProgressManager.CreateOperationProgressEventHandler(Parent, modelCardId, cancellationToken),
cancellationToken
)
.ConfigureAwait(false);

await Commands
.SetModelSendResult(modelCardId, sendResult.RootObjId, sendResult.ConversionResults)
.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
return;
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogModelCardHandledError(ex);
await Commands.SetModelError(modelCardId, ex).ConfigureAwait(false);
}
}

private ICSiWrapper DecodeObjectIdentifier(string encodedId)
{
var (type, name) = ObjectIdentifier.Decode(encodedId);
return CSiWrapperFactory.Create(type, name);
}

public void CancelSend(string modelCardId)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace Speckle.Connectors.CSiShared.HostApp;

/// <summary>
/// Create a centralized access point for ETABS and SAP APIs across the entire program.
/// </summary>
/// <remarks>
/// All API methods are based on the objectType and objectName, not the GUID.
/// CSi is already giving us the "sapModel" reference through the plugin interface. No need to attach to running instance.
/// Since objectType is a single int (1, 2 ... 7) we know first index will always be the objectType.
/// Prevent having to pass the "sapModel" around between classes and this ensures consistent access.
/// Name "sapModel" is misleading since it doesn't only apply to SAP2000, but this is the convention in the API, so we keep it.
/// </remarks>
public interface ICSiApplicationService
{
cSapModel SapModel { get; }
void Initialize(cSapModel sapModel, cPluginCallback pluginCallback);
}

public class CSiApplicationService : ICSiApplicationService
{
public cSapModel SapModel { get; private set; }
private cPluginCallback _pluginCallback;

public CSiApplicationService()
{
SapModel = null!;
}

public void Initialize(cSapModel sapModel, cPluginCallback pluginCallback)
{
SapModel = sapModel;
_pluginCallback = pluginCallback;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.IO;
using Microsoft.Extensions.Logging;
using Speckle.Connectors.DUI.Models;
using Speckle.Connectors.DUI.Utils;
using Speckle.Sdk;
using Speckle.Sdk.Helpers;
using Speckle.Sdk.Logging;

namespace Speckle.Connectors.CSiShared.HostApp;

public class CSiDocumentModelStore : DocumentModelStore
{
private readonly ISpeckleApplication _speckleApplication;
private readonly ILogger<CSiDocumentModelStore> _logger;
private readonly ICSiApplicationService _csiApplicationService;
private string HostAppUserDataPath { get; set; }
private string DocumentStateFile { get; set; }
private string ModelPathHash { get; set; }

public CSiDocumentModelStore(
IJsonSerializer jsonSerializerSettings,
ISpeckleApplication speckleApplication,
ILogger<CSiDocumentModelStore> logger,
ICSiApplicationService csiApplicationService
)
: base(jsonSerializerSettings)
{
_speckleApplication = speckleApplication;
_logger = logger;
_csiApplicationService = csiApplicationService;
SetPaths();
LoadState();
}

private void SetPaths()
{
ModelPathHash = Crypt.Md5(_csiApplicationService.SapModel.GetModelFilepath(), length: 32);
HostAppUserDataPath = Path.Combine(
SpecklePathProvider.UserSpeckleFolderPath,
"ConnectorsFileData",
_speckleApplication.Slug
);
DocumentStateFile = Path.Combine(HostAppUserDataPath, $"{ModelPathHash}.json");
}

protected override void HostAppSaveState(string modelCardState)
{
try
{
if (!Directory.Exists(HostAppUserDataPath))
{
Directory.CreateDirectory(HostAppUserDataPath);
}
File.WriteAllText(DocumentStateFile, modelCardState);
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogError(ex.Message);
}
}

protected override void LoadState()
{
if (!Directory.Exists(HostAppUserDataPath))
{
ClearAndSave();
return;
}

if (!File.Exists(DocumentStateFile))
{
ClearAndSave();
return;
}

string serializedState = File.ReadAllText(DocumentStateFile);
LoadFromString(serializedState);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@

namespace Speckle.Connectors.CSiShared.HostApp;

public sealed class CSiSharedIdleManager : AppIdleManager
public sealed class CSiIdleManager : AppIdleManager
{
private readonly IIdleCallManager _idleCallManager;

public CSiSharedIdleManager(IIdleCallManager idleCallManager)
public CSiIdleManager(IIdleCallManager idleCallManager)
: base(idleCallManager)
{
_idleCallManager = idleCallManager;
}

protected override void AddEvent()
{
// ETABS specific idle handling can be added here if needed
// TODO: CSi specific idle handling can be added here if needed
_idleCallManager.AppOnIdle(() => { });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Speckle.Converters.Common;
using Speckle.Converters.CSiShared;
using Speckle.Sdk.Models.Collections;

namespace Speckle.Connectors.CSiShared.HostApp;

/// <summary>
/// We can use the CSiWrappers to create our collection structure.
/// </summary>
/// <remarks>
/// This class manages the collections. If the key (from the path) already exists, this collection is returned.
/// If it doesn't exist, a new collection is created and added to the rootObject.
/// </remarks>
public class CSiSendCollectionManager
{
private readonly IConverterSettingsStore<CSiConversionSettings> _converterSettings;
private readonly Dictionary<string, Collection> _collectionCache = new();

public CSiSendCollectionManager(IConverterSettingsStore<CSiConversionSettings> converterSettings)
{
_converterSettings = converterSettings;
}

// TODO: Frames could be further classified under Columns, Braces and Beams. Same for Shells which could be classified into walls, floors
public Collection GetAndCreateObjectHostCollection(ICSiWrapper csiObject, Collection rootObject)
Copy link
Member

@clairekuang clairekuang Dec 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be renamed to AddObjectCollectionToRoot, which would be more descriptive.

Why is the root collection being set to the newly created collection? probably can just return the created one directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under commit "AddObjectCollectionToRoot":

  • Agreed. Renamed to AddObjectCollectionToRoot
  • True, unnecessary line removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment probably applies to Revit too Revit - SendCollectionManager.cs

{
var path = csiObject.GetType().Name.Replace("Wrapper", ""); // CSiJointWrapper → CSiJoint, CSiFrameWrapper → CSiFrame etc.

if (_collectionCache.TryGetValue(path, out Collection? value))
{
return value;
}

Collection childCollection = new(path);
rootObject.elements.Add(childCollection);
_collectionCache[path] = childCollection;

rootObject = childCollection;

return rootObject;
}
}

This file was deleted.

Loading
Loading