Files
speckle-sharp-connectors/Connectors/Rhino/Speckle.Connectors.RhinoShared/Operations/Receive/RhinoHostObjectBuilder.cs
T
Jedd Morgan 7860c44f4e feat(api)!: Implement new packfile based sends via SendPipline (aka DuckDB changes) (#1277)
* Dim/quack lets go (#1275)

* Add model ingestion to sharp connectors

* correct ingestion message

* Progress

* grasshopper

* GH exception messages

* fix GH

* file names

* revit file name

* grasshopper file names

* etabs file names

* delete tests

* tekla maybe

* ingestion  scope

* bad boolean logic

* Longer TimeSpan

* wip upload pipe

* 10s

* passthrough ingestion id

* happy hack time: prevent ingestion completion

this is handled server-side in the processing logic.

* add packfile send endpoint detection and routing

Route to SendViaPackfile when the server supports the upload-signing
endpoint (POST probe, 404 = unsupported) and a continuous traversal
builder is registered.

* Adds Continuous Traversal Builder

Introduces a Continuous Traversal Builder to manage the conversion and processing of Revit elements within a Send Pipeline.

---------

Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>

* feat(api): DI Refactor for Duck DB + Gergo's API endpoint changes (#1282)

* Di

* undo accidental change

* Feat (duck): dui ingestion updates post upload (#1295)

* Pass optional ingestion id to DUI

* Make ingestion id null for the SendViaIngestion, see the note :)

* feat!: Duckdev progress reporitng (#1296)

* Di

* throwaway from laptop

* Progress reporting

* Use matching logger

* Revit and revert rhino unpacker progress

* more revertion

* make pr even cleaner

* and this one

* fix build issues with other connectors

* SDK nuget (#1299)

* Bump to 3.14.0-alpha.2

* Feat(duck): grasshopper (#1297)

* Duck x Grasshopper - who would win?

* Fix registration for new builder

* missing imports

* return version id grasshopper

* Align sync resource to sync

---------

Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>

* Bump SDK

* feat(importer): rhino file importer changes for packfile (#1301)

* rhino importer changes

* correct deps

* Bump SDK

* Fix build issues

* ditto

* Fix build issue

* Lower standards

* Fix build

* feat: duck for acad, civil, navis, tekla, etabs (#1300)

* duck: acad, civil, etabs, tekla, navis and bump channels to 10.0.0

* notes

* fix conflicts

* more conflicts

* Ready for testing

* fix(duck): Fix send caching (#1302)

* potential fix

* undo-rhino chnages

* fix xml comment

* amended comment

* revit

* Fix build

* Aligned converting message

* fix: reoccurring object references

* Bump sdk and resolve merge conflict issues

* Merge pull request #1317 from specklesystems/jrm/importer-tracing

feat(otel): Tracing and OTEL changes for Rhino importer

* Fix revit linked model progress (#1312)

* Revert otel packages

* bump SDK

* Trace unpacking groups

* Align trace context nullability with app

* Disable send caching in Navisworks

* comments

* Update FileimportPayload.cs

* fix using directive

---------

Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>

* Fix merge conflicts

---------

Co-authored-by: Dimitrie Stefanescu <didimitrie@gmail.com>
Co-authored-by: Oğuzhan Koral <45078678+oguzhankoral@users.noreply.github.com>
Co-authored-by: Björn Steinhagen <88777268+bjoernsteinhagen@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sebastian Witt <sebastian.witt@rwth-aachen.de>
2026-04-08 10:07:56 +01:00

399 lines
16 KiB
C#

using Rhino;
using Rhino.DocObjects;
using Rhino.Geometry;
using Rhino.Render;
using Speckle.Connectors.Common.Builders;
using Speckle.Connectors.Common.Conversion;
using Speckle.Connectors.Common.Extensions;
using Speckle.Connectors.Common.Operations;
using Speckle.Connectors.Common.Operations.Receive;
using Speckle.Connectors.Common.Threading;
using Speckle.Connectors.Rhino.Extensions;
using Speckle.Connectors.Rhino.HostApp;
using Speckle.Converters.Common;
using Speckle.Converters.Common.ToHost;
using Speckle.Converters.Rhino;
using Speckle.Sdk.Common;
using Speckle.Sdk.Common.Exceptions;
using Speckle.Sdk.Logging;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Collections;
using Speckle.Sdk.Models.Instances;
using Speckle.Sdk.Pipelines.Progress;
namespace Speckle.Connectors.Rhino.Operations.Receive;
/// <summary>
/// <para>Expects to be a scoped dependency per receive operation.</para>
/// </summary>
public class RhinoHostObjectBuilder : IHostObjectBuilder
{
private readonly IRootToHostConverter _converter;
private readonly IConverterSettingsStore<RhinoConversionSettings> _converterSettings;
private readonly RhinoInstanceBaker _instanceBaker;
private readonly RhinoLayerBaker _layerBaker;
private readonly RhinoMaterialBaker _materialBaker;
private readonly RhinoColorBaker _colorBaker;
private readonly RhinoGroupBaker _groupBaker;
private readonly RhinoViewBaker _viewBaker;
private readonly RootObjectUnpacker _rootObjectUnpacker;
private readonly ISdkActivityFactory _activityFactory;
private readonly IThreadContext _threadContext;
private readonly IReceiveConversionHandler _conversionHandler;
private readonly IDataObjectInstanceRegistry _dataObjectInstanceRegistry;
private readonly DataObjectInstanceGrouper _dataObjectInstanceGrouper;
public RhinoHostObjectBuilder(
IRootToHostConverter converter,
IConverterSettingsStore<RhinoConversionSettings> converterSettings,
RhinoLayerBaker layerBaker,
RootObjectUnpacker rootObjectUnpacker,
RhinoInstanceBaker instanceBaker,
RhinoMaterialBaker materialBaker,
RhinoColorBaker colorBaker,
RhinoGroupBaker groupBaker,
RhinoViewBaker viewBaker,
ISdkActivityFactory activityFactory,
IThreadContext threadContext,
IReceiveConversionHandler conversionHandler,
IDataObjectInstanceRegistry dataObjectInstanceRegistry,
DataObjectInstanceGrouper dataObjectInstanceGrouper
)
{
_converter = converter;
_converterSettings = converterSettings;
_rootObjectUnpacker = rootObjectUnpacker;
_instanceBaker = instanceBaker;
_materialBaker = materialBaker;
_colorBaker = colorBaker;
_layerBaker = layerBaker;
_groupBaker = groupBaker;
_viewBaker = viewBaker;
_activityFactory = activityFactory;
_threadContext = threadContext;
_conversionHandler = conversionHandler;
_dataObjectInstanceRegistry = dataObjectInstanceRegistry;
_dataObjectInstanceGrouper = dataObjectInstanceGrouper;
}
#pragma warning disable CA1506
public Task<HostObjectBuilderResult> Build(
#pragma warning restore CA1506
Base rootObject,
string projectName,
string modelName,
IProgress<CardProgress> onOperationProgressed,
CancellationToken cancellationToken
)
{
using var activity = _activityFactory.Start("Build");
// POC: This is where the top level base-layer name is set. Could be abstracted or injected in the context?
var baseLayerName = $"Project {projectName}: Model {modelName}";
// 0 - Clean then Rock n Roll!
PreReceiveDeepClean(baseLayerName);
// 1 - Unpack objects and proxies from root commit object
var unpackedRoot = _rootObjectUnpacker.Unpack(rootObject);
// 2 - Split atomic objects and instance components with their path
var (atomicObjectsWithoutInstanceComponentsForConverter, instanceComponents) =
_rootObjectUnpacker.SplitAtomicObjectsAndInstances(unpackedRoot.ObjectsToConvert);
var atomicObjectsWithoutInstanceComponentsWithPath = _layerBaker.GetAtomicObjectsWithPath(
atomicObjectsWithoutInstanceComponentsForConverter
);
var instanceComponentsWithPath = _layerBaker.GetInstanceComponentsWithPath(instanceComponents);
// 2.1 - these are not captured by traversal, so we need to re-add them here
if (unpackedRoot.DefinitionProxies != null && unpackedRoot.DefinitionProxies.Count > 0)
{
var transformed = unpackedRoot.DefinitionProxies.Select(proxy =>
(Array.Empty<Collection>(), proxy as IInstanceComponent)
);
instanceComponentsWithPath.AddRange(transformed);
}
// 3 - Bake materials and colors, as they are used later down the line by layers and objects
onOperationProgressed.Report(new("Converting materials and colors", null));
if (unpackedRoot.RenderMaterialProxies != null)
{
using var _ = _activityFactory.Start("Render Materials");
_threadContext.RunOnMain(() =>
{
_materialBaker.BakeMaterials(unpackedRoot.RenderMaterialProxies);
});
}
if (unpackedRoot.ColorProxies != null)
{
_colorBaker.ParseColors(unpackedRoot.ColorProxies);
}
// 3.1 - Bake views (Named Views)
if (unpackedRoot.Cameras is not null)
{
_viewBaker.BakeViews(unpackedRoot.Cameras);
}
// 4 - Bake layers
// See [CNX-325: Rhino: Change receive operation order to increase performance](https://linear.app/speckle/issue/CNX-325/rhino-change-receive-operation-order-to-increase-performance)
onOperationProgressed.Report(new("Baking layers (redraw disabled)", null));
using (var _ = _activityFactory.Start("Pre baking layers"))
{
//Rhino 8 doesn't play nice with Eto and layers
_threadContext
.RunOnMain(() =>
{
using var layerNoDraw = new DisableRedrawScope(_converterSettings.Current.Document.Views);
var paths = atomicObjectsWithoutInstanceComponentsWithPath.Select(t => t.path).ToList();
paths.AddRange(instanceComponentsWithPath.Select(t => t.path));
_layerBaker.CreateAllLayersForReceive(paths, baseLayerName);
})
.Wait(cancellationToken);
}
// 5 - Convert atomic objects
var bakedObjectIds = new HashSet<string>();
Dictionary<string, IReadOnlyCollection<string>> applicationIdMap = new(); // This map is used in converting blocks in stage 2. keeps track of original app id => resulting new app ids post baking
HashSet<ReceiveConversionResult> conversionResults = new();
int count = 0;
using (var _ = _activityFactory.Start("Converting objects"))
{
foreach (var (path, obj) in atomicObjectsWithoutInstanceComponentsWithPath)
{
onOperationProgressed.Report(
new("Converting objects", (double)++count / atomicObjectsWithoutInstanceComponentsForConverter.Count)
);
var ex = _conversionHandler.TryConvert(() =>
{
// 0: get pre-created layer from cache in layer baker
int layerIndex = _layerBaker.GetLayerIndex(path, baseLayerName);
cancellationToken.ThrowIfCancellationRequested();
// 1: create object attributes for baking
ObjectAttributes atts = obj.GetAttributes();
atts.LayerIndex = layerIndex;
// 2: convert
var result = _converter.Convert(obj);
// 3: bake
var conversionIds = new List<string>();
if (result is GeometryBase geometryBase)
{
var guid = BakeObject(geometryBase, obj, null, atts);
conversionIds.Add(guid.ToString());
}
else if (result is List<GeometryBase> geometryBases) // one to many raw encoding case
{
// NOTE: I'm unhappy about this case (dim). It's needed as the raw encoder approach can hypothetically return
// multiple "geometry bases" - but this is not a fallback conversion.
// EXTRA NOTE: Oguzhan says i shouldn't be unhappy about this - it's a legitimate case
// EXTRA EXTRA NOTE: TY Ogu, i am no longer than unhappy about it. It's legit "mess".
foreach (var gb in geometryBases)
{
var guid = BakeObject(gb, obj, null, atts);
conversionIds.Add(guid.ToString());
}
}
else if (result is List<(GeometryBase, Base)> fallbackConversionResult) // one to many fallback conversion
{
var guids = BakeObjectsAsFallbackGroup(fallbackConversionResult, obj, atts, baseLayerName);
conversionIds.AddRange(guids.Select(id => id.ToString()));
}
if (conversionIds.Count == 0)
{
// Don't throw if this DataObject was registered for instance baking
if (!_dataObjectInstanceRegistry.IsRegistered(obj.applicationId ?? obj.id.NotNull()))
{
throw new ConversionException("Object did not convert to any native geometry");
}
// Skip normal processing - will be handled by DataObjectInstanceGrouper
return;
}
// 4: log
var id = conversionIds[0]; // this is group id if it is a one to many conversion, otherwise id of object itself
conversionResults.Add(new(Status.SUCCESS, obj, id, result.GetType().ToString()));
if (conversionIds.Count == 1)
{
bakedObjectIds.Add(id);
}
else
{
// first item always a group id if it is a one-to-many,
// we do not want to deal with later groups and its sub elements. It causes a huge issue on performance.
bakedObjectIds.AddRange(conversionIds.Skip(1));
}
// 5: populate app id map
applicationIdMap[obj.applicationId ?? obj.id.NotNull()] = conversionIds;
});
if (ex is not null)
{
conversionResults.Add(new(Status.ERROR, obj, null, null, ex));
}
}
}
// 6 - Convert instances
using (var _ = _activityFactory.Start("Converting instances"))
{
var (createdInstanceIds, consumedObjectIds, instanceConversionResults) = _instanceBaker.BakeInstances(
instanceComponentsWithPath,
applicationIdMap,
baseLayerName,
onOperationProgressed
);
bakedObjectIds.RemoveWhere(id => consumedObjectIds.Contains(id)); // remove all objects that have been "consumed"
bakedObjectIds.UnionWith(createdInstanceIds); // add instance ids
conversionResults.RemoveWhere(result => result.ResultId != null && consumedObjectIds.Contains(result.ResultId)); // remove all conversion results for atomic objects that have been consumed (POC: not that cool, but prevents problems on object highlighting)
conversionResults.UnionWith(instanceConversionResults); // add instance conversion results to our list
}
// 7.1 Group DataObject instances and apply metadata
_dataObjectInstanceGrouper.GroupAndApplyProperties();
// 7.2 Normal group creation
if (unpackedRoot.GroupProxies is not null)
{
_groupBaker.BakeGroups(unpackedRoot.GroupProxies, applicationIdMap, baseLayerName);
}
_converterSettings.Current.Document.Views.Redraw();
return Task.FromResult(new HostObjectBuilderResult(bakedObjectIds, conversionResults));
}
private void PreReceiveDeepClean(string baseLayerName)
{
// Clear DataObject instance registry at start of new build
_dataObjectInstanceRegistry.Clear();
// Remove all previously received layers and render materials from the document
int rootLayerIndex = _converterSettings.Current.Document.Layers.Find(
Guid.Empty,
baseLayerName,
RhinoMath.UnsetIntIndex
);
//Rhino 8 doesn't play nice with Eto and layers
_threadContext
.RunOnMain(() =>
{
_instanceBaker.PurgeInstances(baseLayerName);
// Materials are now reused across receives instead of being purged
// _materialBaker.PurgeMaterials(baseLayerName);
var doc = _converterSettings.Current.Document;
// Cleans up any previously received objects
if (rootLayerIndex != RhinoMath.UnsetIntIndex)
{
var documentLayer = doc.Layers[rootLayerIndex];
var childLayers = documentLayer.GetChildren();
if (childLayers != null)
{
using var layerNoDraw = new DisableRedrawScope(doc.Views);
foreach (var layer in childLayers)
{
var purgeSuccess = doc.Layers.Purge(layer.Index, true);
if (!purgeSuccess)
{
Console.WriteLine($"Failed to purge layer: {layer}");
}
}
}
doc.Layers.Purge(documentLayer.Index, true);
}
// Cleans up any previously received group
_groupBaker.PurgeGroups(baseLayerName);
})
.Wait();
}
/// <summary>
/// Bakes an object to the document.
/// </summary>
/// <param name="obj"></param>
/// <param name="originalObject"></param>
/// <param name="parentObjectId">Parent object ID for color and material proxies search (if fallback conversion was used)</param>
/// <param name="atts"></param>
/// <returns></returns>
/// <remarks>
/// Material and Color attributes are processed here due to those properties existing sometimes on fallback geometry (instead of parent).
/// and this method is called by <see cref="BakeObjectsAsFallbackGroup"/>
/// </remarks>
private Guid BakeObject(GeometryBase obj, Base originalObject, string? parentObjectId, ObjectAttributes atts)
{
var objectId = originalObject.applicationId ?? originalObject.id.NotNull();
if (_materialBaker.ObjectIdAndMaterialIdMap.TryGetValue(objectId, out Guid materialGuid))
{
atts.RenderMaterial = RenderContent.FromId(_converterSettings.Current.Document, materialGuid) as RenderMaterial;
atts.MaterialSource = ObjectMaterialSource.MaterialFromObject;
}
else if (
parentObjectId is not null
&& (_materialBaker.ObjectIdAndMaterialIdMap.TryGetValue(parentObjectId, out Guid parentGuid))
)
{
atts.RenderMaterial = RenderContent.FromId(_converterSettings.Current.Document, parentGuid) as RenderMaterial;
atts.MaterialSource = ObjectMaterialSource.MaterialFromObject;
}
if (_colorBaker.ObjectColorsIdMap.TryGetValue(objectId, out (Color, ObjectColorSource) color))
{
atts.ObjectColor = color.Item1;
atts.ColorSource = color.Item2;
}
else if (
parentObjectId is not null
&& (_colorBaker.ObjectColorsIdMap.TryGetValue(parentObjectId, out (Color, ObjectColorSource) colorSpeckleObj))
)
{
atts.ObjectColor = colorSpeckleObj.Item1;
atts.ColorSource = colorSpeckleObj.Item2;
}
return _converterSettings.Current.Document.Objects.Add(obj, atts);
}
private List<Guid> BakeObjectsAsFallbackGroup(
IEnumerable<(GeometryBase, Base)> fallbackConversionResult,
Base originatingObject,
ObjectAttributes atts,
string baseLayerName
)
{
List<Guid> objectIds = new();
string parentId = originatingObject.applicationId ?? originatingObject.id.NotNull();
int objCount = 0;
foreach (var (conversionResult, originalBaseObject) in fallbackConversionResult)
{
var id = BakeObject(conversionResult, originalBaseObject, parentId, atts);
objectIds.Add(id);
objCount++;
}
// only create groups if we really need to, ie if the fallback conversion result count is bigger than one.
if (objCount > 1)
{
var groupIndex = _converterSettings.Current.Document.Groups.Add(
$"{originatingObject.speckle_type.Split('.').Last()} - {parentId} ({baseLayerName})",
objectIds
);
var group = _converterSettings.Current.Document.Groups.FindIndex(groupIndex);
objectIds.Insert(0, group.Id);
}
return objectIds;
}
}