Summary
I’m building a custom Revit-to-Rhino exporter as a C# Revit add-in using Rhino.Inside. When I call GeometryDecoder.ToBrep(solid) in my add-in, the resulting Brep has IsSolid = false. When I run the exact same conversion logic in a Python script inside Grasshopper (connected to Revit via Rhino.Inside), GeometryDecoder.ToBrep(solid) returns a Closed Brep.
Same method. Same solid. Different result depending on context.
What I’m trying to do
I’m looping through elements visible in a Revit 3D view, extracting their geometry, converting it to Rhino Breps, and writing it to a headless RhinoDoc. The goal is a fully automated .3dm exporter that runs from within Revit.
Solid extraction (C#)
I extract solids from each element like this, recursing into GeometryInstance objects to handle family instances and nested families:
public static IList<Solid> GetSolidsFromElement(Element element)
{
var options = new Options
{
ComputeReferences = true,
IncludeNonVisibleObjects = false
};
options.View = view;
var solids = new List<Solid>();
GeometryElement geomElement = element.get_Geometry(options);
if (geomElement != null)
CollectSolids(geomElement, solids);
return solids;
}
private static void CollectSolids(GeometryElement geomElement, List<Solid> solids)
{
foreach (GeometryObject geomObj in geomElement)
{
switch (geomObj)
{
case Solid solid when solid.Faces.Size > 0 && solid.Volume > 1e-9:
solids.Add(solid);
break;
case GeometryInstance instance:
CollectSolids(instance.GetInstanceGeometry(), solids);
break;
}
}
}
RhinoDoc creation (C#)
//Create the exact same Rhino document as my default RhinoInside.Revit Environment
private static RhinoDoc CreateRhinoDocument()
{
string templatePath = "...\\Large Objects (NLCS) - Millimeters.3dm";
RhinoDoc doc = RhinoDoc.CreateHeadless(templatePath);
// ── Unit System ────────────────────────────────────────────────────────────
doc.ModelUnitSystem = Rhino.UnitSystem.Millimeters;
doc.PageUnitSystem = Rhino.UnitSystem.Millimeters; // Keep layout space in sync
// ── Model Space Tolerances ─────────────────────────────────────────────────
doc.ModelAbsoluteTolerance = 0.001; // 0.001mm — tight enough for BIM
doc.ModelRelativeTolerance = 0.01; // 1%
doc.ModelAngleToleranceDegrees = 1.0; // 1° — fine for BIM
// ── Page (Layout) Space Tolerances ────────────────────────────────────────
doc.PageAbsoluteTolerance = 0.001;
doc.PageRelativeTolerance = 0.01;
doc.PageAngleToleranceDegrees = 1.0;
// ── Display Precision ──────────────────────────────────────────────────────
doc.ModelDistanceDisplayPrecision = 3; // 3 decimal places in model space
doc.PageDistanceDisplayPrecision = 3; // 3 decimal places in layout space
// ── Annotation Scaling ─────────────────────────────────────────────────────
doc.ModelSpaceAnnotationScalingEnabled = true;
doc.LayoutSpaceAnnotationScalingEnabled = true;
doc.ModelSpaceHatchScalingEnabled = true;
doc.ModelSpaceHatchScale = 100.0; // Matches NLCS template default
doc.ModelSpaceTextScale = 1.0;
// ── Meshing ────────────────────────────────────────────────────────────────
// Fast = coarse preview mesh; use Quality or Smooth for accurate export geometry
doc.MeshingParameterStyle = MeshingParameterStyle.Quality;
// ── SubD Display ──────────────────────────────────────────────────────────
doc.SubDAppearance = Rhino.Geometry.SubDComponentLocation.Surface;
// ── Performance (Headless) ─────────────────────────────────────────────────
// Disable undo recording — no user interaction in headless export, saves memory
doc.UndoRecordingEnabled = false;
return doc;
}
Conversion (C#)
foreach (Solid solid in solids)
{
Brep brep = GeometryDecoder.ToBrep(solid);
// brep != null, geometry looks correct, dimensions correct, units correct
// but: brep.IsSolid == false
}
I also tried calling brep.JoinNakedEdges(doc.ModelAbsoluteTolerance) and brep.Repair(doc.ModelAbsoluteTolerance) afterwards — neither changed the result.
The same logic in Grasshopper / Python — works perfectly
Running this in a GHPython component inside Grasshopper, connected to Revit via Rhino.Inside:
from Autodesk.Revit.DB import Options, GeometryInstance, Solid
from RhinoInside.Revit.Convert.Geometry import GeometryDecoder
def get_solids_from_element(element):
options = Options()
options.ComputeReferences = True
options.IncludeNonVisibleObjects = False
options.View = element.Document.ActiveView
solids = []
geom_element = element.get_Geometry(options)
if geom_element is None:
return solids
_collect_solids(geom_element, solids)
return solids
def _collect_solids(geom_element, solids):
for geom_obj in geom_element:
if isinstance(geom_obj, Solid):
if geom_obj.Faces.Size > 0 and geom_obj.Volume > 1e-9:
solids.append(geom_obj)
elif isinstance(geom_obj, GeometryInstance):
_collect_solids(geom_obj.GetInstanceGeometry(), solids)
solids = get_solids_from_element(x)
a = [brep for s in solids if (brep := GeometryDecoder.ToBrep(s)) is not None]
The Grasshopper panel output confirms Closed Brep for all results.
Questions
-
Is there a way to tell
GeometryDecoderwhichRhinoDocto use as its context, so it joins faces with the correct tolerance? -
Is there an officially supported pattern for using
GeometryDecoderoutside of the Grasshopper runtime — i.e., in a standalone Revit add-in with a headlessRhinoDoc? -
Am I missing an initialisation step for Rhino.Inside that would set the active document context correctly before calling
GeometryDecoder?
Any guidance appreciated. Thanks.