BRepBuilder and Toposurface Interior

An updated Revit SDK, an in-depth discussion of Revit geometry generation, a Toposurface issue and a hint at where the software development industry may be headed:

Revit SDK Update

The age-old developrevit Revit Developer Centre shortcut now redirects to a new target aligned with the rest of the Autodesk Platform Service APS APIs, to

There, you can now find the new Revit SDK for Revit 2024 updated on May 30, 2023, to fix some of the issues I described in April compiling the Revit 2024 SDK samples and subsequently reported in the ticket REVIT-206304 Update RvtSamples.txt for Revit 2024 SDK.

BRepBuilder Organisation

Richard RPThomas108 Thomas and Luiz Henrique @ricaun Cassettari share some illuminating insights on using the BRepBuilder addressing why BRepBuilder fails on very simple example:

Question: I get a "Failure" result on a very simple BRepBuilder example when I try to build a (4-sided triangle pyramid) Tetrahedron. I know about TessellatedShapeBuilder, but it fails for a quite complex object, and I hope to bypass this using the BREP instead.

public class Test{

  public Test(){}

  // Keep track of already created edges and their orientation
  struct BrepEdge {
    public BRepBuilderGeometryId id;
    public XYZ p1, p2;
  }
  List<BrepEdge> brep_edges = new List<BrepEdge>();

  // add edge to face loop
  void AddEdgeToBREP( BRepBuilder brep, BRepBuilderGeometryId loop, XYZ a, XYZ b) {
    foreach(var be in brep_edges) {
      // ab is p1-p2
      if(be.p1.DistanceTo(a) < 1e-7 && be.p2.DistanceTo(b) < 1e-7) { brep.AddCoEdge(loop, be.id, false);return; }
      // ab is p2-p1 (reversed edge)
      if(be.p1.DistanceTo(b) < 1e-7 && be.p2.DistanceTo(a) < 1e-7) { brep.AddCoEdge(loop, be.id, true); return; }
    }
    // must create a new edge
    BRepBuilderGeometryId edge = brep.AddEdge(BRepBuilderEdgeGeometry.Create(a, b));
    brep.AddCoEdge(loop, edge, false);
    var bed = new BrepEdge();
    bed.p1 = a;
    bed.p2 = b;
    bed.id = edge;
    brep_edges.Add(bed);
  }

  // add triangle face to solid
  private void AddTriangleToBREP( BRepBuilder brep, XYZ a, XYZ b, XYZ c ) {
    Plane plane = Plane.CreateByThreePoints(a, b, c);
    BRepBuilderGeometryId face = brep.AddFace(BRepBuilderSurfaceGeometry.Create(plane, null), true);
    var loop = brep.AddLoop(face);
    AddEdgeToBREP(brep, loop, a, b);
    AddEdgeToBREP(brep, loop, b, c);
    AddEdgeToBREP(brep, loop, c, a);
    brep.FinishLoop(loop);
    brep.FinishFace(face);
  }

  public void run() {
    BRepBuilder brep = new BRepBuilder(BRepType.Solid);

    var points = new List<XYZ>(4);
    points.Add(new XYZ(0, 0, 0)); // 0 origin
    points.Add(new XYZ(1, 0, 0)); // 1 right
    points.Add(new XYZ(0, 1, 0)); // 2 back
    points.Add(new XYZ(0, 0, 1)); // 3 top

    AddTriangleToBREP(brep, points[2], points[1], points[0]); // bottom face
    AddTriangleToBREP(brep, points[0], points[1], points[3]); // front face
    AddTriangleToBREP(brep, points[1], points[2], points[3]); // diagonal face
    AddTriangleToBREP(brep, points[2], points[0], points[3]); // left face

    var outcome = brep.Finish(); // <<<<< Failure

    // throws: "This BRepBuilder object hasn't completed building data or was unsuccessful building it.
    // Built Geometry is unavailable. In order to access the built Geometry,
    // Finish() must be called first. That will set the state to completed."
    var res = brep.GetResult();
  }
}

Answer: DirectShape from BrepBuilder and Boolean provides an example that does work.

However, please note that BRepBuilder wasn’t really meant for 'manually' constructing geometry. Its interface is rather cumbersome for that purpose. It was meant for translating existing geometry into Revit, with rather thorough validation of the input geometry.

Response: I implemented a workaround using TesselatedShapeBuilder, but it's strange that I can't get the simplest example working with the BREP commands. Also, the error message is not very helpful.

Later: I experienced the same error when try to create a simple pyramid shape as a prototype for recreating geometry from linked IFC file. I would like to use some external algorithm the geometry and convert the external data to a mesh and rebuilt the new shape. The API documentation does not provide much of information about why it failed.

Answer: It takes a bit of organisation but it does work.

It isn't ideal for manual creation since (1) you need to arrange the edges so they are compatible and (2) the edges need to sit on the surface.

I organised the above original into the below edge orders and directions and it worked fine. Each co-edge should be reversed on one face and not reversed on the other. Outer loops for a face should be anticlockwise with respect to the face normal (pointing outwards for a solid). So the edge is indicated as reversed on the face if it can't satisfy that e.g. the edge of two adjoining faces needs to be reversed on one of them.

BRep organisation

  Private Function Obj_230606a( _
    ByVal commandData As ExternalCommandData,
    ByRef message As String,
    ByVal elements As ElementSet) As Result

    Dim UIApp As UIApplication = commandData.Application
    Dim UIDoc As UIDocument = commandData.Application.ActiveUIDocument
    If UIDoc Is Nothing Then Return Result.Cancelled Else
    Dim IntDoc As Document = UIDoc.Document

    ' Only needs 6 edges '
    ' Set on triangle if an edge of that triangle is reversed or not '
    ' Each edge has two faces the edge should be reversed on one face and not on the other '
    ' Outer loops should be anticlockwise with respect to normal '

    Dim A As New TriangleSide(1, 0)
    Dim B As New TriangleSide(0, 2)
    Dim C As New TriangleSide(2, 1)
    Dim D As New TriangleSide(1, 3)
    Dim E As New TriangleSide(3, 0)
    Dim F As New TriangleSide(3, 2)

    Dim T_ABC As New Triangle(A, B, C, New Boolean(2) {False, False, False})
    Dim T_ADE As New Triangle(A, D, E, New Boolean(2) {True, False, False})
    Dim T_BEF As New Triangle(B, E, F, New Boolean(2) {True, True, False})
    Dim T_CFD As New Triangle(C, F, D, New Boolean(2) {True, True, True})

    Dim Triangles As Triangle() = New Triangle(3) {T_ABC, T_ADE, T_BEF, T_CFD}
    Dim Edges As TriangleSide() = New TriangleSide(5) {A, B, C, D, E, F}

    ' The coords '

    Dim X As Double = 1
    Dim Y As Double = 1
    Dim Z As Double = 1

    Dim Points As XYZ() = New XYZ(3) {
      New XYZ(0, 0, 0),
      New XYZ(X, 0, 0),
      New XYZ(0, Y, 0),
      New XYZ(0, 0, Z)}

    Dim BrepB As New BRepBuilder(BRepType.Solid)

    ' Faces '

    For i = 0 To Triangles.Length - 1
      Dim T As Triangle = Triangles(i)
      Dim P As Plane = T.GetPlane(Points)
      Triangles(i).FaceId = BrepB.AddFace(BRepBuilderSurfaceGeometry.Create(P, Nothing), False)
    Next

    ' Edges '

    For i = 0 To Edges.Length - 1
      Dim ed As TriangleSide = Edges(i)
      Edges(i).EdgeId = BrepB.AddEdge(BRepBuilderEdgeGeometry.Create(ed.GetXYZ(0, Points), ed.GetXYZ(1, Points)))
    Next

    ' Face loops '

    For i = 0 To Triangles.Length - 1
      Dim T As Triangle = Triangles(i)
      Triangles(i).LoopId = BrepB.AddLoop(T.FaceId)
    Next

    ' Co-edges '

    For i = 0 To Triangles.Length - 1
      Dim T As Triangle = Triangles(i)

      For ia = 0 To 2
        Dim ed As TriangleSide = T(ia)
        BrepB.AddCoEdge(T.LoopId, ed.EdgeId, T.Sides_Reversed(ia))
      Next
      BrepB.FinishLoop(T.LoopId)
      BrepB.FinishFace(T.FaceId)
    Next

    Dim Outcome As BRepBuilderOutcome = BrepB.Finish

    Using Tx As New Transaction(IntDoc, "DS")
      If Tx.Start = TransactionStatus.Started Then

        Dim DS As DirectShape = DirectShape.CreateElement(IntDoc, New ElementId(BuiltInCategory.OST_GenericModel))
        Dim S As Solid = BrepB.GetResult()
        DS.SetShape(New GeometryObject() {S}.ToList)

        Tx.Commit()
      End If
    End Using

    Return Result.Succeeded
  End Function

  Public Class TriangleSide
    Public Property EdgeId As BRepBuilderGeometryId
    Public ReadOnly Property N1 As Integer
    Public ReadOnly Property N2 As Integer
    Public Function GetXYZ(Idx As Integer, Points As XYZ()) As XYZ
      If Idx = 0 Then
        Return Points(N1)
      ElseIf Idx = 1 Then
        Return Points(N2)
      Else
        Throw New ArgumentOutOfRangeException
      End If
    End Function
    Public Sub New(Node1 As Integer, Node2 As Integer)
      N1 = Node1
      N2 = Node2
    End Sub
  End Class

  Public Class Triangle
    Public Property LoopId As BRepBuilderGeometryId
    Public Property FaceId As BRepBuilderGeometryId
    Public ReadOnly Property Sides_Reversed As Boolean()
    Public ReadOnly Property S1 As TriangleSide
    Public ReadOnly Property S2 As TriangleSide
    Public ReadOnly Property S3 As TriangleSide

    Public Function GetPlane(Points As XYZ()) As Plane

      ' The edge ends could be reversed for a triangles, so use '
      ' midpoints to ensure points are unique and not colinear '

      Dim Mid1 As XYZ = (Points(S1.N1) + Points(S1.N2)) / 2
      Dim Mid2 As XYZ = (Points(S2.N1) + Points(S2.N2)) / 2
      Dim Mid3 As XYZ = (Points(S3.N1) + Points(S3.N2)) / 2

      Return Plane.CreateByThreePoints(Mid1, Mid2, Mid3)
    End Function

    Default Public ReadOnly Property Side(Idx As Integer) As TriangleSide
      Get
        If Idx = 0 Then
          Return S1
        ElseIf Idx = 1 Then
          Return S2
        ElseIf Idx = 2 Then
          Return S3
        Else
          Throw New ArgumentOutOfRangeException
        End If
      End Get
    End Property

    Public Sub New(Side1 As TriangleSide, Side2 As TriangleSide, Side3 As TriangleSide, Reversed As Boolean())
      S1 = Side1
      S2 = Side2
      S3 = Side3

      If Reversed.Length <> 3 Then
        Throw New ArgumentOutOfRangeException
      End If
      Sides_Reversed = Reversed

    End Sub
  End Class

Response: It took me a while to figure out how the edges and faces should be added before the detail explanation and sample code reaches me. Thank you very much for your effort.

That is a really cool image; did you create it or you found in some Revit API presentation?

Is kinda missing a Four-sided dice in that image 😀

Another thing that is good to mention, is if your BRepType is Void, all the face normals must point into the void.

Answer: Thanks @ricaun I drew it with AutoCAD after a couple of attempts at what I wanted to get across using pencil, paper and eraser (more eraser than pencil and paper).

I'll have to check how important it is to set the boundaries of the surfaces. I'm sure I noticed previously and the other day that just specifying null may create a performance issue in Revit after the direct shape exists.

The best thing about the BRepBuilder in my view is that you can create single face solids. So all those API functions specific to the face class can be used on such.

Answer: Regarding the boundaries, you say:

I'll have to check how important it is to set the boundaries of the surfaces. I'm sure I noticed previously and the other day that just specifying null may create a performance issue in Revit after the direct shape exists.

In my code, the boundaries of the surfaces are null as well, never have a problem but the most complex thing I did was create a copy of a Solid to change the Material. Works, but reordering and reversing each edge is painful.

@RPTHOMAS108 wrote: The best thing about the BRepBuilder in my view is that you can create single face solids. So all those API functions specific to the face class can be used on such.

For a simple face, it is possible to use TessellatedShapeBuilder: you can create a Solid, or a Mesh if that fails.

In this D4 is much simpler to use than TessellatedFace – D4 as in tetrahedron, a shape with 4 flat faces, a dice with 4 faces, or a D4 if you are familiar with dice and RPG. 😀

var shapeBuilder
  = TessellatedShapeCreatorUtils.Create(
    builder =>
  {
    var points = new List<XYZ>(4);
    points.Add(new XYZ(0, 0, 0)); // 0 origin
    points.Add(new XYZ(1, 0, 0)); // 1 right
    points.Add(new XYZ(0, 1, 0)); // 2 back
    points.Add(new XYZ(0, 0, 1)); // 3 top

    var materialId = ElementId.InvalidElementId;
    // bottom face
    builder.AddFace(new TessellatedFace(new[] {
      points[2], points[1], points[0] }, materialId));
    // front face
    builder.AddFace(new TessellatedFace(new[] {
      points[0], points[1], points[3] }, materialId));
    // diagonal face
    builder.AddFace(new TessellatedFace(new[] {
      points[1], points[2], points[3] }, materialId));
    // left face
    builder.AddFace(new TessellatedFace(new[] {
      points[2], points[0], points[3] }, materialId));
  });

Here is the full code of TessellatedShapeCreatorUtils.cs implementing this utility class

public static class TessellatedShapeCreatorUtils
{
  public static TessellatedShapeBuilderResult Create(
    Action<TessellatedShapeBuilder> actionBuilder)
  {
    TessellatedShapeBuilder builder = new TessellatedShapeBuilder();
    builder.Target = TessellatedShapeBuilderTarget.AnyGeometry;
    builder.Fallback = TessellatedShapeBuilderFallback.Mesh;
    builder.OpenConnectedFaceSet(true);
    actionBuilder?.Invoke(builder);
    builder.CloseConnectedFaceSet();
    builder.Build();
    TessellatedShapeBuilderResult result = builder.GetBuildResult();
    return result;
  }
}

If you are working with a Revit surface, BRepBuilder is the way to go.

Many thanks to Richard and Luiz Henrique for the good advice, illuminating discussion and great sample code!

Change Toposurface Interior Point to Boundary

The long-standing question on Toposurface – change interior point to boundary point finally receives a clear and succinct suggestion for a solution by Mitchell Currie of Struxi:

Question: Is there any way to change an interior point to a boundary point or vice versa using Revit's API? I want to do this so I can display the boundary of the topography I have generated correctly.

Toposurface boundary and interior points

Answer: Use a subregion and hide it.

Thank you for the answer!

To Code or Not to Code, That is the Question

An interesting comparison of two programming approaches in modern times by Ab Advany:

To code or not to code

Supervised a project 2 week ago. Two programmers were hired to create an MVP. I have worked with both before:

What do you think happened?

Both programmers received Figma screens and detailed specs. A designer was available to assist them with the assets they needed, and there was also existing code that needed to be integrated. Hamid finished the first version in one week with 100% test coverage of code and end-to-end testing of no-code parts. 95% of the work seems to be finished and appears to work at first glance... 😲 Hamid built the UI and front-end workflows in bubble, generated Cloudflare Workers using GPT-4, integrated existing code using Copilot, and generated tests using GPT-4 (playwright/ava).

Hamid's costs:

Alex finished around 7% of the tasks. Costs:

This was a project I supervised for a friend of mine with an agency. I only did it because I was really curious about how it would turn out.

We both thought that Hamid would be finished in 8-10 weeks while Alex would take a week or two extra, but the results amazed us!

He had a talk with Alex about this. His response? "But it will be so much cheaper to run this app, and you'll have everything under control." Not understanding the opportunity costs of shipping 13 times slower and 25 times more expensive to develop.

Alex was let go because he wants to "code" and doesn't trust no-code/AI...

The development agency of my friend has 100+ developers like Alex. Now he is going to retrain or replace them with developers like Hamid...

I think people like Hamid will still have work five years from now, while people like Alex will have to find other jobs/professions. What do you think?