From 4b65aa438612986d331bc2fbafa0f28b35cdeff8 Mon Sep 17 00:00:00 2001 From: Alan Rynne Date: Tue, 10 Dec 2024 23:52:07 +0100 Subject: [PATCH] feat: POC Send node --- .../SpeckleTaskCapableComponent.cs | 27 +++- .../Operations/Send/SendComponent.cs | 124 ++++++++++++++++++ .../HostApp/GrasshopperGooExtensions.cs | 33 +++++ .../HostApp/GrasshopperSendOperation.cs | 74 +++++++++++ .../HostApp/SpeckleResource.cs | 35 +++++ .../PriorityLoader.cs | 13 +- .../ServiceRegistration.cs | 25 ++++ .../ToSpeckle/Meshing/DisplayMeshExtractor.cs | 51 ++++--- .../BrepToSpeckleTopLevelConverter.cs | 41 ++++++ 9 files changed, 391 insertions(+), 32 deletions(-) create mode 100644 Connectors/Rhino/Speckle.Connectors.Grasshopper8/Components/Operations/Send/SendComponent.cs create mode 100644 Connectors/Rhino/Speckle.Connectors.Grasshopper8/HostApp/GrasshopperGooExtensions.cs create mode 100644 Connectors/Rhino/Speckle.Connectors.Grasshopper8/HostApp/GrasshopperSendOperation.cs create mode 100644 Connectors/Rhino/Speckle.Connectors.Grasshopper8/ServiceRegistration.cs create mode 100644 Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/TopLevel/BrepToSpeckleTopLevelConverter.cs diff --git a/Connectors/Rhino/Speckle.Connectors.Grasshopper8/Components/BaseComponents/SpeckleTaskCapableComponent.cs b/Connectors/Rhino/Speckle.Connectors.Grasshopper8/Components/BaseComponents/SpeckleTaskCapableComponent.cs index 24c141415..5f0cf6b2a 100644 --- a/Connectors/Rhino/Speckle.Connectors.Grasshopper8/Components/BaseComponents/SpeckleTaskCapableComponent.cs +++ b/Connectors/Rhino/Speckle.Connectors.Grasshopper8/Components/BaseComponents/SpeckleTaskCapableComponent.cs @@ -27,11 +27,28 @@ public abstract class SpeckleTaskCapableComponent( catch (SpeckleException e) { Console.WriteLine(e); + AddRuntimeMessage(GH_RuntimeMessageLevel.Error, e.Message); } return; } - if (!GetSolveResults(da, out TOutput result)) + bool solveResults = false; + TOutput? result = default; + + try + { + solveResults = GetSolveResults(da, out result); + } + catch (AggregateException e) + { + Console.WriteLine(e); + foreach (var inner in e.InnerExceptions) + { + AddRuntimeMessage(GH_RuntimeMessageLevel.Error, inner.Message); + } + } + + if (!solveResults) { // INFO: This will run synchronously. Useful for Rhino.Compute runs, but can also be enabled by user. try @@ -40,9 +57,12 @@ public abstract class SpeckleTaskCapableComponent( var syncResult = PerformTask(input).Result; result = syncResult; } - catch (SpeckleException e) + catch (AggregateException e) { - Console.WriteLine(e); + foreach (var inner in e.InnerExceptions) + { + AddRuntimeMessage(GH_RuntimeMessageLevel.Error, inner.Message); + } return; } } @@ -56,6 +76,7 @@ public abstract class SpeckleTaskCapableComponent( protected override Bitmap Icon => BitmapBuilder.CreateSquareIconBitmap(IconText); protected string IconText => string.Join("", Name.Split(' ').Select(s => s.First())); + protected abstract TInput GetInput(IGH_DataAccess da); protected abstract void SetOutput(IGH_DataAccess da, TOutput result); diff --git a/Connectors/Rhino/Speckle.Connectors.Grasshopper8/Components/Operations/Send/SendComponent.cs b/Connectors/Rhino/Speckle.Connectors.Grasshopper8/Components/Operations/Send/SendComponent.cs new file mode 100644 index 000000000..1ec656ef7 --- /dev/null +++ b/Connectors/Rhino/Speckle.Connectors.Grasshopper8/Components/Operations/Send/SendComponent.cs @@ -0,0 +1,124 @@ +using Grasshopper.Kernel; +using Grasshopper.Kernel.Data; +using Grasshopper.Kernel.Types; +using Microsoft.Extensions.DependencyInjection; +using Rhino; +using Speckle.Connectors.Common.Operations; +using Speckle.Connectors.Grasshopper8.Components.BaseComponents; +using Speckle.Connectors.Grasshopper8.HostApp; +using Speckle.Connectors.Grasshopper8.Parameters; +using Speckle.Converters.Common; +using Speckle.Converters.Rhino; +using Speckle.Sdk; +using Speckle.Sdk.Api; +using Speckle.Sdk.Common; +using Speckle.Sdk.Credentials; + +namespace Speckle.Connectors.Grasshopper8.Components.Operations.Send; + +public class SendComponentInput +{ + public SpeckleUrlModelResource Resource { get; } + public Dictionary> Inputs { get; } + + public SendComponentInput(SpeckleUrlModelResource resource, Dictionary> inputs) + { + Resource = resource; + Inputs = inputs; + } +} + +public class SendComponentOutput(SpeckleUrlModelObjectResource resource) +{ + public SpeckleUrlModelObjectResource Resource { get; } = resource; +} + +public class SendComponent() + : SpeckleScopedTaskCapableComponent( + "Send", + "SS", + "Speckle Send", + "Speckle", + "Operations" + ) +{ + public override Guid ComponentGuid => new("0CF0D173-BDF0-4AC2-9157-02822B90E9FB"); + + protected override void RegisterInputParams(GH_InputParamManager pManager) + { + pManager.AddParameter(new SpeckleUrlModelResourceParam()); + pManager.AddGenericParameter("A", "A", "A", GH_ParamAccess.tree); + } + + protected override void RegisterOutputParams(GH_OutputParamManager pManager) + { + pManager.AddParameter(new SpeckleUrlModelResourceParam()); + } + + protected override SendComponentInput GetInput(IGH_DataAccess da) + { + if (da.Iteration != 0) + { + throw new SpeckleException("No more than 1 resource allowed"); + } + + SpeckleUrlModelResource? resource = null; + if (!da.GetData(0, ref resource)) + { + throw new SpeckleException("Failed to get resource"); + } + + var name = Params.Input[1].Name; + da.GetDataTree(1, out GH_Structure tree); + + var inputDict = new Dictionary> { { name, tree } }; + + return new SendComponentInput(resource.NotNull(), inputDict); + } + + protected override void SetOutput(IGH_DataAccess da, SendComponentOutput result) + { + da.SetData(0, result.Resource); + } + + protected override async Task PerformScopedTask( + SendComponentInput input, + IServiceScope scope, + CancellationToken cancellationToken = default + ) + { + var rhinoConversionSettingsFactory = scope.ServiceProvider.GetRequiredService(); + scope + .ServiceProvider.GetRequiredService>() + .Initialize(rhinoConversionSettingsFactory.Create(RhinoDoc.ActiveDoc)); + + var accountManager = scope.ServiceProvider.GetRequiredService(); + var clientFactory = scope.ServiceProvider.GetRequiredService(); + var sendOperation = scope.ServiceProvider.GetRequiredService(); + + // TODO: Get any account for this server, as we don't have a mechanism yet to pass accountIds through + var account = accountManager.GetAccountWithServerUrlFallback("", new Uri(input.Resource.Server)); + + if (account is null) + { + throw new SpeckleAccountManagerException($"No default account was found"); + } + + var progress = new Progress(_ => + { + // TODO: Progress only makes sense in non-blocking async receive, which is not supported yet. + // Message = $"{progress.Status}: {progress.Progress}"; + }); + + using var client = clientFactory.Create(account); + var receiveInfo = await input.Resource.GetSendInfo(client, cancellationToken).ConfigureAwait(false); + + var result = await sendOperation + .Execute(input.Inputs, receiveInfo, progress, cancellationToken) + .ConfigureAwait(false); + + return new SendComponentOutput( + new SpeckleUrlModelObjectResource(receiveInfo.ServerUrl.ToString(), receiveInfo.ProjectId, result.RootId) + ); + } +} diff --git a/Connectors/Rhino/Speckle.Connectors.Grasshopper8/HostApp/GrasshopperGooExtensions.cs b/Connectors/Rhino/Speckle.Connectors.Grasshopper8/HostApp/GrasshopperGooExtensions.cs new file mode 100644 index 000000000..2ee8459d2 --- /dev/null +++ b/Connectors/Rhino/Speckle.Connectors.Grasshopper8/HostApp/GrasshopperGooExtensions.cs @@ -0,0 +1,33 @@ +using System.Reflection; +using Grasshopper.Kernel.Types; +using Speckle.Sdk; + +namespace Speckle.Connectors.Grasshopper8.HostApp; + +public static class GrasshopperGooExtensions +{ + public static T UnwrapGoo(this IGH_Goo goo) + { + if (goo is GH_Goo specificGoo) + { + return specificGoo.Value; + } + + var valuePropInfo = goo.GetType() + .GetField("m_value", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + + if (valuePropInfo != null) + { + var tempValue = valuePropInfo.GetValue(goo); + if (tempValue is T value) + { + return value; + } + } + // TODO: Potentially unwrap new rhino objects + + throw new SpeckleException( + $"Internal value of goo {goo.GetType().Name} was not the provided type {typeof(T).Name}" + ); + } +} diff --git a/Connectors/Rhino/Speckle.Connectors.Grasshopper8/HostApp/GrasshopperSendOperation.cs b/Connectors/Rhino/Speckle.Connectors.Grasshopper8/HostApp/GrasshopperSendOperation.cs new file mode 100644 index 000000000..397427479 --- /dev/null +++ b/Connectors/Rhino/Speckle.Connectors.Grasshopper8/HostApp/GrasshopperSendOperation.cs @@ -0,0 +1,74 @@ +using Grasshopper.Kernel.Data; +using Grasshopper.Kernel.Types; +using Rhino.Geometry; +using Speckle.Connectors.Common.Operations; +using Speckle.Converters.Common; +using Speckle.Sdk; +using Speckle.Sdk.Common; +using Speckle.Sdk.Models; +using Speckle.Sdk.Models.Collections; + +namespace Speckle.Connectors.Grasshopper8.HostApp; + +public class GrasshopperSendOperation +{ + private readonly IRootObjectSender _baseObjectSender; + private readonly IRootToSpeckleConverter _converter; + + public GrasshopperSendOperation(IRootObjectSender baseObjectSender, IRootToSpeckleConverter converter) + { + _baseObjectSender = baseObjectSender; + _converter = converter; + } + + public async Task Execute( + IReadOnlyDictionary> objects, + SendInfo sendInfo, + IProgress onOperationProgressed, + CancellationToken ct = default + ) + { + var elements = new List(); + foreach (var keypair in objects) + { + elements.Add(ConvertGhStructureToCollection(keypair.Key, keypair.Value)); + } + + var buildResult = new Collection("Grasshopper Model") { elements = elements, ["version"] = 3 }; + + var result = await _baseObjectSender.Send(buildResult, sendInfo, onOperationProgressed, ct).ConfigureAwait(false); + + return new(result.RootId); + } + + private Collection ConvertGhStructureToCollection(string name, GH_Structure tree) + { + var result = tree.Paths.Select(path => + { + var pathElements = tree.get_Branch(path) as IList; + var convertedElements = new List(); + + pathElements.NotNull(); + + foreach (var pathElement in pathElements) + { + try + { + var goo = pathElement.UnwrapGoo(); + var converted = _converter.Convert(goo); + convertedElements.Add(converted); + } + catch (Exception e) when (!e.IsFatal()) + { + Console.WriteLine(e); + } + } + + return new Collection(path.ToString()) { elements = convertedElements }; + }); + + return new Collection(name) { elements = result.Cast().ToList() }; + } +} + +public record GrashopperSendOperationResult(string RootId); diff --git a/Connectors/Rhino/Speckle.Connectors.Grasshopper8/HostApp/SpeckleResource.cs b/Connectors/Rhino/Speckle.Connectors.Grasshopper8/HostApp/SpeckleResource.cs index 6c2effad5..0878edd03 100644 --- a/Connectors/Rhino/Speckle.Connectors.Grasshopper8/HostApp/SpeckleResource.cs +++ b/Connectors/Rhino/Speckle.Connectors.Grasshopper8/HostApp/SpeckleResource.cs @@ -9,6 +9,8 @@ namespace Speckle.Connectors.Grasshopper8.HostApp; public abstract record SpeckleUrlModelResource(string Server, string ProjectId) { public abstract Task GetReceiveInfo(Client client, CancellationToken cancellationToken = default); + + public abstract Task GetSendInfo(Client client, CancellationToken cancellationToken = default); } public record SpeckleUrlLatestModelVersionResource(string Server, string ProjectId, string ModelId) @@ -35,6 +37,21 @@ public record SpeckleUrlLatestModelVersionResource(string Server, string Project return info; } + + public override async Task GetSendInfo(Client client, CancellationToken cancellationToken = default) + { + // We don't care about the return info, we just want to be sure we have access and everything exists. + await client.Project.Get(ProjectId, cancellationToken).ConfigureAwait(false); + await client.Model.Get(ModelId, ProjectId, cancellationToken).ConfigureAwait(false); + + return new SendInfo( + client.Account.id, + new Uri(Server), + ProjectId, + ModelId, + "Grasshopper8" // TODO: Grab from the right place! + ); + } } public record SpeckleUrlModelVersionResource(string Server, string ProjectId, string ModelId, string VersionId) @@ -59,6 +76,21 @@ public record SpeckleUrlModelVersionResource(string Server, string ProjectId, st return info; } + + public override async Task GetSendInfo(Client client, CancellationToken cancellationToken = default) + { + // We don't care about the return info, we just want to be sure we have access and everything exists. + await client.Project.Get(ProjectId, cancellationToken).ConfigureAwait(false); + await client.Model.Get(ModelId, ProjectId, cancellationToken).ConfigureAwait(false); + + return new SendInfo( + client.Account.id, + new Uri(Server), + ProjectId, + ModelId, + "Grasshopper8" // TODO: Grab from the right place! + ); + } } public record SpeckleUrlModelObjectResource(string Server, string ProjectId, string ObjectId) @@ -66,4 +98,7 @@ public record SpeckleUrlModelObjectResource(string Server, string ProjectId, str { public override Task GetReceiveInfo(Client client, CancellationToken cancellationToken = default) => throw new NotImplementedException("Object Resources are not supported yet"); + + public override Task GetSendInfo(Client client, CancellationToken cancellationToken = default) => + throw new NotImplementedException("Object Resources are not supported yet"); } diff --git a/Connectors/Rhino/Speckle.Connectors.Grasshopper8/PriorityLoader.cs b/Connectors/Rhino/Speckle.Connectors.Grasshopper8/PriorityLoader.cs index ace930747..26be2d1f3 100644 --- a/Connectors/Rhino/Speckle.Connectors.Grasshopper8/PriorityLoader.cs +++ b/Connectors/Rhino/Speckle.Connectors.Grasshopper8/PriorityLoader.cs @@ -1,14 +1,9 @@ using Grasshopper.Kernel; using Microsoft.Extensions.DependencyInjection; using Speckle.Connectors.Common; -using Speckle.Connectors.Common.Builders; -using Speckle.Connectors.Common.Operations.Receive; -using Speckle.Connectors.Grasshopper8.HostApp; using Speckle.Converters.Rhino; using Speckle.Sdk; -using Speckle.Sdk.Credentials; using Speckle.Sdk.Host; -using Speckle.Sdk.Models.GraphTraversal; namespace Speckle.Connectors.Grasshopper8; @@ -23,15 +18,9 @@ public class PriorityLoader : GH_AssemblyPriority { var services = new ServiceCollection(); _disposableLogger = services.Initialize(HostApplications.Grasshopper, GetVersion()); - services.AddRhinoConverters().AddConnectorUtils(); - services.AddTransient(); - services.AddTransient(); - services.AddSingleton(DefaultTraversal.CreateTraversalFunc()); - services.AddScoped(); + services.AddRhinoConverters().AddGrasshopper().AddConnectorUtils(); - services.AddTransient(); - services.AddTransient(); Container = services.BuildServiceProvider(); return GH_LoadingInstruction.Proceed; } diff --git a/Connectors/Rhino/Speckle.Connectors.Grasshopper8/ServiceRegistration.cs b/Connectors/Rhino/Speckle.Connectors.Grasshopper8/ServiceRegistration.cs new file mode 100644 index 000000000..bc5e513ad --- /dev/null +++ b/Connectors/Rhino/Speckle.Connectors.Grasshopper8/ServiceRegistration.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using Speckle.Connectors.Common.Builders; +using Speckle.Connectors.Common.Operations.Receive; +using Speckle.Connectors.Grasshopper8.HostApp; +using Speckle.Sdk.Credentials; +using Speckle.Sdk.Models.GraphTraversal; + +namespace Speckle.Connectors.Grasshopper8; + +public static class ServiceRegistration +{ + public static IServiceCollection AddGrasshopper(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddSingleton(DefaultTraversal.CreateTraversalFunc()); + services.AddScoped(); + + services.AddTransient(); + services.AddTransient(); + + return services; + } +} diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Meshing/DisplayMeshExtractor.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Meshing/DisplayMeshExtractor.cs index e35c82757..d8ce34374 100644 --- a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Meshing/DisplayMeshExtractor.cs +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/Meshing/DisplayMeshExtractor.cs @@ -11,27 +11,44 @@ public static class DisplayMeshExtractor var renderMeshes = obj.GetMeshes(RG.MeshType.Render); if (renderMeshes.Length == 0) { - switch (obj) - { - case BrepObject brep: - renderMeshes = RG.Mesh.CreateFromBrep(brep.BrepGeometry, new(0.05, 0.05)); - break; - case ExtrusionObject extrusion: - renderMeshes = RG.Mesh.CreateFromBrep(extrusion.ExtrusionGeometry.ToBrep(), new(0.05, 0.05)); - break; - case SubDObject subDObject: -#pragma warning disable CA2000 - var mesh = RG.Mesh.CreateFromSubD(subDObject.Geometry as RG.SubD, 0); -#pragma warning restore CA2000 - renderMeshes = [mesh]; - break; - default: - throw new ConversionException($"Unsupported object for display mesh generation {obj.GetType().FullName}"); - } + renderMeshes = GetDisplayMeshes(obj.Geometry); } var joinedMesh = new RG.Mesh(); joinedMesh.Append(renderMeshes); return joinedMesh; } + + public static RG.Mesh GetDisplayMesh(RG.GeometryBase obj) + { + // note: unsure this is nice, we get bigger meshes - we should to benchmark (conversion time vs size tradeoffs) + var renderMeshes = GetDisplayMeshes(obj); + var joinedMesh = new RG.Mesh(); + joinedMesh.Append(renderMeshes); + return joinedMesh; + } + + private static RG.Mesh[] GetDisplayMeshes(RG.GeometryBase obj) + { + RG.Mesh[] renderMeshes; + switch (obj) + { + case RG.Brep brep: + renderMeshes = RG.Mesh.CreateFromBrep(brep, new(0.05, 0.05)); + break; + case RG.Extrusion extrusion: + renderMeshes = RG.Mesh.CreateFromBrep(extrusion.ToBrep(), new(0.05, 0.05)); + break; + case RG.SubD subDObject: +#pragma warning disable CA2000 + var mesh = RG.Mesh.CreateFromSubD(subDObject, 0); +#pragma warning restore CA2000 + renderMeshes = [mesh]; + break; + default: + throw new ConversionException($"Unsupported object for display mesh generation {obj.GetType().FullName}"); + } + + return renderMeshes; + } } diff --git a/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/TopLevel/BrepToSpeckleTopLevelConverter.cs b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/TopLevel/BrepToSpeckleTopLevelConverter.cs new file mode 100644 index 000000000..5cb604d26 --- /dev/null +++ b/Converters/Rhino/Speckle.Converters.RhinoShared/ToSpeckle/TopLevel/BrepToSpeckleTopLevelConverter.cs @@ -0,0 +1,41 @@ +using Speckle.Converters.Common; +using Speckle.Converters.Common.Objects; +using Speckle.Converters.Rhino.ToSpeckle.Encoding; +using Speckle.Converters.Rhino.ToSpeckle.Meshing; +using Speckle.Sdk.Models; + +namespace Speckle.Converters.Rhino.ToSpeckle.TopLevel; + +[NameAndRankValue(nameof(RG.Brep), NameAndRankValueAttribute.SPECKLE_DEFAULT_RANK)] +public class BrepToSpeckleTopLevelConverter : IToSpeckleTopLevelConverter +{ + private readonly ITypedConverter _meshConverter; + private readonly IConverterSettingsStore _settingsStore; + + public BrepToSpeckleTopLevelConverter( + ITypedConverter meshConverter, + IConverterSettingsStore settingsStore + ) + { + _meshConverter = meshConverter; + _settingsStore = settingsStore; + } + + public Base Convert(object target) + { + var brepObject = (RG.Brep)target; + var brepEncoding = RawEncodingCreator.Encode(brepObject, _settingsStore.Current.Document); + + var mesh = DisplayMeshExtractor.GetDisplayMesh(brepObject); + var displayValue = new List { _meshConverter.Convert(mesh) }; + + var bx = new SOG.BrepX() + { + displayValue = displayValue, + encodedValue = brepEncoding, + units = _settingsStore.Current.SpeckleUnits + }; + + return bx; + } +}