Rhino to Revit Converter - Include Data

Hi everyone,

We are trialing a few import / export loops between Rhino and Revit and we have not managed to transfer any data, along the geometry. I’d love to hear from the community how they handle transfer of data between the two platforms.

:arrow_forward: The situation is as follows :

  • We model a 3D brep in Rhino.

  • We add custom data through an in-house plugin

  • We send to a stream via Rhino connector

  • We receive stream via Revit connector

:pause_button: Now the problem is the gradual loss of data along the way :

  • Rhino element contains rhino data and plugin data

  • Speckle element contains rhino data

  • Revit element contains no data

I am aware there is a Grasshopper solution for sending Speckle Stream with custom data, but that data would still not be imported in Revit - is there something we’re missing, when it comes to data transfer? Ideally we would love to be able to work with the out of the box connectors.

Thanks for your help and this great open source community!

If by data you mean Shared/Project parameters as instance data on Revit elements, this is not a supported conversion.

We have heard many voices from both sides on whether Speckle should create attributes on Revit objects where none existed previously. Currently, we are conservatively supporting the BIM managers seeking to keep their models relatively data-clean.

If Speckle were to consider making this additional optional, this raises a question I’d ask: how would you anticipate seeing Rhino user data presented in the Revit environment? Arbitrary Text attributes ?, Identity data? Data attributes?


The option for using Grasshopper is helpful because you can publish Revit project information to a speckle stream, consume this in GH and then populate parameters you know to exist in the target Revit model.

Another option open to you is to augment data to existing parameters (or new ones) using Dynamo. Dynamo will have access to both the project’s parameters and speckle data. You can either receive the rhino objects in Revit and then augment the data properties or try to use Dynamo entirely for the element creation (this is a lot more work)

Hi Jonathon,

Thanks for that quick answer, very helpful !

This is a good tip. Only viable fallback plan I’ve seen for now. I’m assuming I could only fetch the default rhino data (not the custom rhino data). In which case we’ll have to do 5 steps :

  1. Export Rhino geometry and Rhino default data to speckle

  2. Export Rhino custom data to something else (csv, db, …)

  3. Import geometry from Speckle to Revit with the speckle connector

  4. Import rhino guid data from Speckle to Revit with the dynamo speckle connector

  5. Import Rhino custom data to Revit and map to correct object through GUID imported at step 4

That’s a good question ; I do see your point. I would be in the camp of allowing more and cleaning up afterwards, our tech team can canvas the workflow and limit our plugins parameters to avoid the dirty files as much as possible. That said, have you considered a middle ground, where only one parameter is imported, and it’s the Guid of the source object form the original authoring platform (in this case Rhino) ?

With such a GUID being kept from original platform to destination platform, we could create quick and dirty data transfer solutions without having to modify the Speckle connector or push design teams down the route of GH / Dynamo.

Thanks again for your input

This would only be necessary if your custom Rhino plugin adds data to the inner object level as proprietary plugins like VisualArq does. Indeed, Speckle does not support this as we don’t have access to the libraries that extended the Rhino object, nor are they likely to be open-source.

If your plugin could append data with the now much more standard user-data model, this would be in Speckle.

We all see the benefits when it suits our current needs :spockle_smirk: I trust you implicitly, but what about those pesky interns downloading all possible arbitrary data sets?

Not specifically, IDs are hard. But, I take your point on the benefit in this case.

Amen to that!

If your plugin could append data with the now much more standard user-data model, this would be in Speckle.

That’s what our plugin does, so that’s encouraging news, I havent seen the data popup on the Speckle web viewer, but I have yet to interrogate the streams from an API call.

Thanks again for your input. And congrats on the amazing work with Speckle, really well thought through and as simple-looking as it gets (which is anything but simple).

3 Likes

It is the community comments like these that keep us smiling (after 6pm, on a Friday, on our days off :winking_spockle: )

And in case anyone at A&M remembers me - say hi from me :wave:

The BIM team - now rebranded the Design Technology Team :robot: - says hello too.
Thanks for the time you spent discussing Speckle at the office last year.

1 Like

Update on the solution we found

The challenge we tried to solve was :

We produce masses and floors in our Rhino in-house toolkit. We want to transfer them as native Revit masses and floors with parameters and areas that can be scheduled.

The solutions we investigated :

  • Dynamo Speckle, Rhino.Inside and Grasshopper Speckle.
    These were discarded immediately, as we wanted our colleagues that are not technically minded to be able to click one button and it works magically.

  • Write our own ultra lightweight geometry conversion algorithms from scratch.
    We serialize the Rhino geometries into XAML, then deserialize it in Revit, using the Revit API and their brep builder to build native editable goemetry in BIM. We definitely did not foresee this to be so challenging. The slightest mishaps in tolerance or the smallest gaps and nothing was working.

  • Write a modified version of the Speckle Revit connector.
    This ended up being the solution. We used the Speckle Revit Connector code available on GitHub (thanks!) and modified the Receive commands to reroute some of the conversion algorithms for floors and masses, to create native floors from Rhino floor breps and to transfer the Rhino UserStrings as Revit project parameters. We didn’t want to rely on a long list of dependencies and thus were hesitant to call on the Speckle-Sharp SDK for most of the conversion (dll hell and all of that). But it turned out to be the better avenue.



Some snippets of the changes we made to the Speckle Connector Code.

// THIS FUNCTION IS TRIGGERED BY A BUTTON IN OUR UI.
// WHEN THE BUTTON IS CLICKED, THIS FUNCTION HANDLES :
// THE LOGGING IN SPECKLE VIA AccountManager.AddAccount()
// THE RETRIEVAL OF THE USER'S STREAMS VIA SpeckleClient.StreamsGet()
// THE SELECTION OF THE USER'S STREAMS VIA A UI DIALOG UserLastStreamSelection()
// THE AUTOMATIC RETRIEVAL OF THE LAST COMMIT VIA SpeckleClient.StreamGetCommits()
// THE AUTOMATIC RETRIEVAL OF THE STREAM DATA VIA Helpers.Receive()
// THE CONVERSION OF THE STREAM DATA IS HANDLED IN A REVIT TRANSACTION OUTSIDE OF THIS FUNCTION

internal async Task<bool> RetrieveLastStream()
{
    if (SpeckleClient == null)
    {
        CurrentAccount = AccountManager.GetDefaultAccount();

        if (CurrentAccount == null)
        {
            await AccountManager.AddAccount();

            CurrentAccount = AccountManager.GetDefaultAccount();

            if (CurrentAccount == null)
            {
                MessageBox.Show("Please log in with Speckle to transfer");
                return false;
            }
        }

        SpeckleClient = new Client(CurrentAccount);
    }

    if (SelectedProject == null)
    {
        var streams = await SpeckleClient.StreamsGet();
        var lastStream = await UserLastStreamSelection(streams);
        if (lastStream == null)
            return false;
        SelectedProject = lastStream;
    }

    var commits = await SpeckleClient.StreamGetCommits(SelectedProject.id);

    var lastCommit = commits[0];

    var cid = lastCommit.id;

    var fullUrl = $"{SpeckleClient.ServerUrl}/streams/{SelectedProject.id}/commits/{cid}";

    var streamData = await Helpers.Receive(fullUrl);

    if (streamData == null)
    {
        MessageBox.Show("No data found");
        return false;
    }

    await ConverterSetup(streamData);

    return true;
}
// this is one of the main functions available in the Speckle Connector, changed.
private ApplicationObject ConvertObject(ApplicationObject obj, Base @base, bool receiveDirectMesh, ConverterRevit converter, TransactionManager transactionManager)
{
    if (obj == null || @base == null)
        return obj;

    transactionManager.StartSubtransaction();

    try
    {
        ApplicationObject convRes;

// HERE IS A CHANGE WE MAKE, CHECK IF THE SPECKLE OBJECT IS A FLOOR AS DEFINED BY OUR IN-HOUSE RHINO TOOLKIT, VIA USER STRINGS
        if (IsFloor(@base))
        {
            convRes = FloorToNative(@base, converter);
        }

        else
        {
            // continue with normal specKle conversion
        }

// HERE IS ANOTHER CHANGE WE MAKE, WE TRANSFER THE USER STRINGS PROPERTIES TO REVIT PROPERTIES
        TransferParameters(@base, convRes);

    }
    catch (Exception e)
    {

    }
}

// THIS IS A REWRITE OF THE ORIGINAL SPECKLE FUNCTION, TO CREATE A NATIVE REVIT FLOOR FROM A GENERIC RHINO BREP
public ApplicationObject FloorToNative(Base baseFloor, ConverterRevit converter)
{
    Autodesk.Revit.DB.Element existingElementByApplicationId = null;

    ApplicationObject applicationObject = new ApplicationObject(baseFloor.id, baseFloor.speckle_type)
    {
        applicationId = baseFloor.applicationId
    };

    if (baseFloor is Brep brep)
    {

        var faces = brep.Faces.Where(x => x != null).Where(f => f.Surface != null).OrderByDescending(x => x.Surface?.bbox?.zSize?.end).ToList();
        
        // THIS IS IMPORTANT, WE PICK THE TOP FACE OF THE BREP, FROM WHICH WE CAN EXTRACT THE OUTLINE TO CREATE NATIVE FLOOR
        var face2d = faces
                        .OrderByDescending(x => Math.Min((double)x.Surface?.domainU.Length, (double)x.Surface?.domainV.Length))
                        .ThenByDescending(f => converter
                                        .CurveToNative(f?.OuterLoop?.Trims?.Select(t => t?.Edge?.Curve).ToList())?
                                        .get_Item(0)?
                                        .GetEndPoint(0)
                                        .Z)
                        .FirstOrDefault();

        if (face2d == null)
        {
            applicationObject.Update(null, null, ApplicationObject.State.Failed);
            applicationObject.Log.Add("No valid face found in brep.");
            return applicationObject;
        }

        var outline = face2d.OuterLoop;

        if (outline == null)
        {
            applicationObject.Update(null, null, ApplicationObject.State.Failed);
            applicationObject.Log.Add("No valid outline found in face.");
            return applicationObject;
        }

        var outerLoopCurves = new List<ICurve>();


        // WE EXTRACT THE CURVES OF THE OUTLINE
        foreach (var trim in outline.Trims)
        {
            var curve = trim.Edge?.Curve;
            if (curve != null)
                outerLoopCurves.Add(curve);
        }

        // WE CHECK IF THERE'S ANY INNER LOOPS
        var innerLoops = face2d.Loops.Except(new List<BrepLoop> { outline }).ToList();

        var innerLoopCurves = new List<List<ICurve>>();

        if (innerLoops != null && innerLoops.Count > 0)
        {
            foreach (var loop in innerLoops)
            {
                var innerLoop = new List<ICurve>();
                foreach (var trim in loop.Trims)
                {
                    var curve = trim.Edge?.Curve;
                    if (curve != null)
                        innerLoop.Add(curve);
                }

                innerLoopCurves.Add(innerLoop);
            }
        }

        ApplicationObject.State state = ApplicationObject.State.Unknown;

        double elevationOffset = 0.0;


        // WE GET THE ELEVATION POINT OF THE CURVE, AS PER SPECKLE ORIGINAL FUNCTION
        var startPoint = converter.CurveToNative(outerLoopCurves)?.get_Item(0)?.GetEndPoint(0);
        var level = converter.ConvertLevelToRevit(startPoint, out state, out elevationOffset);

        CurveArray tempArray = new CurveArray();
        foreach (var c in outerLoopCurves)
        {
            ICurve flattenedCurve = GetFlattenedCurve(c, level.Elevation);

            try
            {
                CurveArray ca = converter.CurveToNative(flattenedCurve, splitIfClosed: true);
                foreach (Autodesk.Revit.DB.Curve curve in ca)
                {
                    tempArray.Append(curve);
                }
            }
            catch (Exception ex)
            {
                applicationObject.Update(null, null, ApplicationObject.State.Failed);
                applicationObject.Log.Add(ex.Message);
                return applicationObject;
            }
        }

        // THIS METHOD IS LINKED TO OUR UI, WHERE THE USER PICKS THE FLOOR TYPE THEY WANT TO USE
        ElementId floorTypeId = RevitMethods.FindSelectedSlabType();
        FloorType elementType = AppSetup.ActiveDoc.GetElement(floorTypeId) as FloorType;

        Autodesk.Revit.DB.Floor floor = null;

        List<Autodesk.Revit.DB.CurveLoop> list = new List<Autodesk.Revit.DB.CurveLoop> { converter.CurveArrayToCurveLoop(tempArray) };

        if (innerLoopCurves != null && innerLoopCurves.Count > 0)
        {
            foreach (var innerLoop in innerLoopCurves)
            {
                CurveArray innerLoopArray = new CurveArray();
                foreach (var c in innerLoop)
                {
                    ICurve flattenedCurve = GetFlattenedCurve(c, level.Elevation);
                    CurveArray ca = converter.CurveToNative(flattenedCurve, splitIfClosed: true);

                    foreach (Autodesk.Revit.DB.Curve curve in ca)
                    {
                        innerLoopArray.Append(curve);
                    }
                }

                list.Add(converter.CurveArrayToCurveLoop(innerLoopArray));
            }
        }

        // CREATE THE FLOOR IN REVIT
        floor = Autodesk.Revit.DB.Floor.Create(AppSetup.ActiveDoc, list, elementType.Id, level.Id, false, null, 0.0);

        Autodesk.Revit.DB.Parameter offset = floor.get_Parameter(BuiltInParameter.FLOOR_HEIGHTABOVELEVEL_PARAM);
        offset.Set(-elevationOffset);

        AppSetup.ActiveDoc.Regenerate();

        // TRANSFER ALL NECESSARY PARAMETERS
        var ps = GetMasterplanParameters(baseFloor);
        RevitMethods.SyncParametersFloorsMass(floor, ps);

        applicationObject.Update(status: ApplicationObject.State.Created, createdId: floor.UniqueId, createdIds: null, container: null, log: null, logItem: null, converted: null, convertedItem: floor);
        return applicationObject;

    }

    return applicationObject;
}
2 Likes

Great stuff @Romain_Bigare .

We may pick your brains when we look at the next iterations of the Mapper project.

1 Like