GeometryDecoder.ToBrep(Solid): open Brep in C#, closed in Grasshopper

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

  1. Is there a way to tell GeometryDecoder which RhinoDoc to use as its context, so it joins faces with the correct tolerance?

  2. Is there an officially supported pattern for using GeometryDecoder outside of the Grasshopper runtime — i.e., in a standalone Revit add-in with a headless RhinoDoc?

  3. Am I missing an initialisation step for Rhino.Inside that would set the active document context correctly before calling GeometryDecoder?

Any guidance appreciated. Thanks.

RhinoDocCreatedWithRhinoInsideRevit(Good).3dm (100.7 KB)

RhinoDocCreatedWithAddin(NotGood).3dm (1.8 MB)

Hi @Joel14,

Unfortunately your intuition is right.
These methods are ment to be used with the active Rhino document.

When this API was written there were no headless documents so all are assuming units and tolerances of the active document.

I take note to improve this.

Right now, and as a workaround, you can setup your headless document using same units and tolerances as the active document, do all your conversions, add them to your document, and before exporting you should call AdjustModelUnitSystem passing true on the last argument to scale everything to the desired units.

Hope it helps.

Hi Kike, thanks for the workaround suggestion. I’ve implemented it but the issue persists, and I’ve narrowed down where it actually breaks.

Two things I tried based on your suggestion:

  1. Mirroring all unit system and tolerance settings from RhinoDoc.ActiveDoc onto the headless document before converting — no change.

  2. Extracting the project length unit from the Revit document and applying that to the headless document — also no change

My conversion method works as follows: it first attempts GeometryDecoder.ToBrep(solid) directly. If the result is not solid, it falls back to converting each face individually via face.ToBrep(), collects all face Breps, and joins them using Brep.JoinBreps with RhinoDoc.ActiveDoc.ModelAbsoluteTolerance as the join tolerance. If the joined result is still not solid, it makes a final attempt with brep.JoinNakedEdges at ten times the tolerance before giving up and returning the open Brep.

With logging on each face conversion I can see that face.ToBrep() itself returns IsValid = false for a consistent subset of faces on every failing solid, across all element types. These invalid face Breps are the root cause — they can’t be joined regardless of tolerance and leave holes in the result.

My suspicion is that the invalid faces point to something deeper than units or tolerances — likely a missing initialisation step that Grasshopper performs automatically but a standalone add-in does not, which affects how GeometryDecoder builds the internal Brep representation.

Both document settings produce the same invalid face Breps on the same faces.

Question: Is there an initialisation step for Rhino.Inside (beyond creating the headless doc) that sets up the internal conversion context — something that Grasshopper does automatically but a standalone add-in does not?

Thanks.

@Joel14,

Could you please share some Revit geometry that is failing on your side. I will try to reproduce it here.

Thanks.

@kike

Here is some geometry which I am testing. Some simple and some more complex geometry

SampleGeometry.rvt (536 KB)