Render Text via Direct Context 3D

Render Text via Direct Context 3D

Rendering text in the Revit API using DirectContext3D can be approached in several ways, each with its advantages and limitations. Here, we explore two methods: FormattedTextand GraphicsPath.

FormattedText

FormattedText.BuildGeometry is a practical choice for rendering text. It converts text into a geometry that can be rendered in 3D. However, it doesn't always match the font shape perfectly, but it offers acceptable performance for many applications.

Advantages:

  • Better performance.

  • Simpler implementation.

  • Generates a reasonable approximation of text shapes.

Disadvantages:

  • Limited accuracy in matching the font's exact shape.

  • Converts curves to polyline segments, which might not always be desirable.

Implementation Example:

List<CurveLoop> rvtCurves = [];

    var formatted = fontSettings.GetFormattedText(message);
    GeometryGroup geo = formatted.BuildGeometry(new System.Windows.Point()) as GeometryGroup;
    var pathGeometry = geo.GetFlattenedPathGeometry();

    foreach (var fig in pathGeometry.Figures)
    {
        var cloop = new CurveLoop();

        XYZ startPoint = fig.StartPoint.ToRvtPoint();
        for (int i = 0; i < fig.Segments.Count; i++)
        {
            PathSegment seg = fig.Segments[i];
            try
            {
                Curve curve = null;
                if (seg is LineSegment lineSeg)
                {
                    curve = Line.CreateBound(startPoint, lineSeg.Point.ToRvtPoint());
                }
                else if (seg is BezierSegment bezSeg)
                {
                    var p0 = startPoint;
                    var p1 = bezSeg.Point1.ToRvtPoint();
                    var p2 = bezSeg.Point2.ToRvtPoint();
                    var p3 = bezSeg.Point3.ToRvtPoint();
                    curve = HermiteSpline.Create([p0, p1, p2, p3], false);
                }
                else if (seg is PolyLineSegment polSeg)
                {
                    var points = polSeg.Points.Select(o => o.ToRvtPoint()).ToList();
                    points.Insert(0, startPoint);
                    foreach (var item in points.DrawAPI(false))
                    {
                        cloop.Append(item);
                    }
                }
                else if (seg is PolyBezierSegment polBezSeg)
                {
                    var points = polBezSeg.Points.Select(o => o.ToRvtPoint()).ToList();
                    points.Insert(0, startPoint);
                    var tangents = new HermiteSplineTangents();
                    tangents.StartTangent = (points[1] - points[0]).Normalize();
                    tangents.EndTangent = (points[3] - points[2]).Normalize();
                    curve = HermiteSpline.Create([points[0], points[3]], periodic: false, tangents);
                    cloop.Append(curve);
                }
                else
                {
                    // Unsupported type yet
                }

                if (curve != null)
                {
                    cloop.Append(curve);
                    startPoint = curve.GetEndPoint(1);
                }
            }
            catch (Exception ex)
            {
                // something went wrong
            }
        }

        rvtCurves.Add(cloop);
    }
}

GraphicsPath

GraphicsPath offers a closer match to the original font shapes but at a significant cost in terms of performance. It generates a higher number of points and curves, leading to a denser and more accurate geometry representation.

Red is FormattedText
Blue is GraphicsPath

Advantages:

  • Higher accuracy in font shape rendering.

  • Better for applications where visual fidelity is critical.

Disadvantages:

  • Higher computational cost.

  • Increased number of faces and points can lead to performance issues.

Implementation example

List<CurveLoop> rvtCurveLoops = [];
try
{
    // Create a GraphicsPath object
    GraphicsPath path = new GraphicsPath();
    path.AddString(
        message,
        new System.Drawing.FontFamily(fontSettings.Family.FamilyNames.First().Value),
        (int)fontSettings.FontStyle,
        fontSettings.FontSize,
        new PointF(),
        StringFormat.GenericDefault
    );

    // Create lists to hold points and types
    PointF[] points = path.PathPoints;
    byte[] types = path.PathTypes;

    // Initialize a list to hold the resulting Revit curves
    List<Curve> revitCurves = new List<Curve>();

    // Temporary lists to store points for each curve
    List<XYZ> linePoints = new List<XYZ>();
    List<XYZ> BezierPoints = new List<XYZ>();

    XYZ startPoint = points[0].ToRvtPoint();

    for (int i = 1; i < points.Length; i++)
    {
        // Convert PointF to XYZ
        XYZ point = points[i].ToRvtPoint();
        byte type = types[i];

        switch (type)
        {
            case (byte)PathPointType.Start:

                startPoint = point;
                if (revitCurves.Any())
                {
                    rvtCurveLoops.Add( CurveLoop.Create(revitCurves));
                    revitCurves.Clear();
                }
                break;

            case (byte)PathPointType.Line:
                linePoints.Add(startPoint);
                linePoints.Add(point);
                startPoint = linePoints.Last();

                AddLineCurves(linePoints, revitCurves);
                linePoints.Clear();
                break;

            case (byte)PathPointType.Bezier3:
                BezierPoints.Add(startPoint);
                BezierPoints.Add(point);
                BezierPoints.Add(points[++i].ToRvtPoint());
                BezierPoints.Add(points[++i].ToRvtPoint());
                startPoint = BezierPoints.Last();
                AddBezierCurves(BezierPoints, revitCurves);
                BezierPoints.Clear();
                break;

            case (byte)PathPointType.CloseSubpath:
                   // $"{PathPointType.CloseSubpath} not implemented"
                break;

            case (byte)PathPointType.PathMarker:
                    //$"{PathPointType.PathMarker} not implemented"
                break;

            case (byte)PathPointType.DashMode:
                    //$"{PathPointType.DashMode} not implemented"
                break;

            default:
                // Handle any other types if necessary
                break;
        }
    }
    if (revitCurves.Any())
    {
         rvtCurveLoops.Add( CurveLoop.Create(revitCurves));
    }

    return rvtCurveLoops;

Performance Comparison:

  • GraphicsPath Method:

    • Points: 104

    • Curves: 35

  • FormattedText Method:

    • Points: 47

    • Curves: 47

Example Results:

  • Red: FormattedText output.

  • Blue: GraphicsPath output.

  • font used: Ink Free

In conclusion, while FormattedText.BuildGeometry offers better performance, GraphicsPath provides more accurate text shapes. The choice between them depends on the specific requirements of accuracy versus performance in your application.

Red is FormattedText
Blue is GraphicsPath

However using GraphicsPath, usually returns a broken graphics, not sure why yet.

Edit 11th June

I was able to realize the why FormattedText shows different optimized shape than GraphicsPath, due to the face we are using this statement

var pathGeometry = geo.GetFlattenedPathGeometry();

This statement accoriding to Documentation The polygonal approximation of the geometry. thus if we changed the tollerance of flattening, it would return a better result.

 var figures = geo.GetFlattenedPathGeometry(.00001, ToleranceType.Relative)
                  .Figures;

This is using FormattedText with flattengeometry of .00001 almost the maximum. which returned exactly what is expected

Now we play with tolerance to suit the performance against quality as it is always every developer challenge

Forgot to mention... that when converting to solid, then we can have what the font size is actually rendered on revit. due to the fact that each font scales differently, we can use Transform.ScaleBasis(value), in the direct context 3D, to scale it to the size we need saying intendedWidth should be 1 meter long

// extention function are helpful here
scale = intendedWidth/textSolid.GetBoundingBox().Width;

an example of how text can be helpful during design

Did you find this article valuable?

Support Moustafa Khalil by becoming a sponsor. Any amount is appreciated!