Rectangular Duct Corners

In between the series of background information from Scott's Autodesk University presentation on analysing building geometry, let's have a quick look at a practical application.

This is an MEP-specific issue on how to determine the corners of a rectangular duct. One approach to determine this information is to perform the following steps:

We explored the geometry of various elements in detail in the past, e.g. slab boundary, slab side faces, wall elevation profile, 2D polygon areas and outer loop, 3D polygon areas, and cylindrical columns. We have also had a look at MEP connectors and used the information contained in them, e.g. for the modeless pressure drop tool. This analysis now shows how these two sources of information, geometry and connectors, can be usefully combined.

The following solution comes from a case handled by Joe Ye and an implementation by Aaron and Vico in the Revit development team.

Question: I need to determine the four corners at the end of a rectangular duct. Given a connector at the end of the duct, I have been using the connector coordinate system transform and width and height properties to determine the corners like this:

  XYZ p = connector.CoordinateSystem.OfPoint( 
    new XYZ( connector.Width / 2, 
      connector.Height / 2, 0 ) );

This only seems to work if the duct is oriented such that the airflow is in the XY plane.

If the duct's airflow is along the Z axis so the duct is going up or down, I can calculate the corner like this:

  XYZ p = connector.CoordinateSystem.OfPoint( 
    new XYZ( connector.Height / 2, 
      connector.Width / 2, 0 ) );

I seem to be missing something, because it seems like I shouldn't have to determine the duct's orientation. Is there a more general solution to determining the corners of a rectangular duct? Also, if the duct has been rotated, neither of these methods works.

Answer: There is no direct access to the four corners, but they can easily be determined from the duct geometry in combination with the connector location:

I added a new external command CmdRectDuctCorners to The Building Coder sample application to demonstrate how these steps can be implemented.

This implementation also demonstrates making use of the .NET diagnostics Trace class to create a log file to write the data to, rather than using our customary approach of writing to the Visual Studio debug console window. Using the Trace class and writing to a file is useful for driving automated tests and comparing the results with an expected template output file, for instance.

We make use of the following helper methods:

Here is the implementation of GetFirstRectangularConnector:

static bool GetFirstRectangularConnector( 
  Duct duct, 
  out Connector c1 )
{
  c1 = null;
 
  ConnectorSet connectors 
    = duct.ConnectorManager.Connectors;
 
  if( 0 < connectors.Size )
  {
    foreach( Connector c in connectors )
    {
      if( ConnectorProfileType.RectProfile 
        == c.Shape )
      {
        c1 = c;
        break;
      }
      else
      {
        Trace.WriteLine( "Connector shape: " 
          + c.Shape );
      }
    }
  }
  return null != c1;
}

FaceContainsConnector is short and sweet. It uses Face.Project to determine the closest point on the face to the connector origin. If it exists and the distance returned by the method is small, the connector is contained in the face:

static bool FaceContainsConnector(
  Face face,
  Connector c )
{
  XYZ p = c.Origin;
 
  IntersectionResult result = face.Project( p );
 
  return null != result
    && Math.Abs( result.Distance ) < 1e-9;
}

The mainline of the command does the following:

Application app = commandData.Application;
 
if( ProductType.MEP != app.Product )
{
  message = "Please run this command in Revit MEP.";
  return CmdResult.Failed;
}
 
Document doc = app.ActiveDocument;
SelElementSet sel = doc.Selection.Elements;
 
if( 0 == sel.Size )
{
  message = "Please select some rectangular ducts.";
  return CmdResult.Failed;
}
 
// set up log file:
 
string log = Assembly.GetExecutingAssembly().Location 
  + "." + DateTime.Now.ToString( "yyyyMMdd" ) 
  + ".log";
 
if( File.Exists( log ) )
{
  File.Delete( log );
}
 
TraceListener listener 
  = new TextWriterTraceListener( log );
 
Trace.Listeners.Add( listener );
 
try
{
  Trace.WriteLine( "Begin" );
 
  // loop over all selected ducts:
 
  foreach( Duct duct in sel )
  {
    if( null == duct )
    {
      Trace.TraceError( "The selection is not a duct!" );
    }
    else
    {
      // process each duct:
 
      Trace.WriteLine( "========================" );
      Trace.WriteLine( "Duct: Id = " + duct.Id.Value );
 
      AnalyseDuct( duct );
    }
  }
}
catch( Exception ex )
{
  Trace.WriteLine( ex.ToString() );
}
finally
{
  Trace.Flush();
  listener.Close();
  Trace.Close();
  Trace.Listeners.Remove( listener );
}
return CmdResult.Failed;

Finally, here is the AnalyseDuct method that does the real work:

static bool AnalyseDuct( Duct duct )
{
  bool rc = false;
 
  Connector c1;
  if( !GetFirstRectangularConnector( duct, out c1 ) )
  {
    Trace.TraceError( "The duct is not rectangular!" );
  }
  else
  {
    Options opt = new Options();
    opt.DetailLevel = Options.DetailLevels.Fine;
    GeoElement geoElement = duct.get_Geometry( opt );
 
    foreach( GeometryObject obj in geoElement.Objects )
    {
      Solid solid = obj as Solid;
      if( solid != null )
      {
        bool foundFace = false;
        foreach( Face face in solid.Faces )
        {
          foundFace = FaceContainsConnector( face, c1 );
          if( foundFace )
          {
            Trace.WriteLine( "==> Four face corners:" );
 
            EdgeArray a = face.EdgeLoops.get_Item( 0 );
 
            foreach( Edge e in a )
            {
              XYZ p = e.Evaluate( 0.0 );
 
              Trace.WriteLine( "Point = " 
                + Util.PointString( p ) );
            }
            rc = true;
            break;
          }
        }
        if( !foundFace )
        {
          Trace.WriteLine( "[Error] Face not found" );
        }
      }
    }
  }
  return rc;
}

This is the resulting log file output after running this command with a simple rectangular duct selected:

Begin
========================
Duct: Id = 431526
==> Four face corners:
Point = (-18.27,4.66,8.53)
Point = (-18.27,4.66,9.51)
Point = (-18.27,3.67,9.51)
Point = (-18.27,3.67,8.53)

Here is version 1.1.0.58 of the complete Building Coder sample source code and Visual Studio solution including the new command.

Many thanks to Joe for handling this case and to Aaron and Vico for the initial implementation!