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;
}