Add an Icon and text in Rhino's canvas

Hi everyone,

I am working on developing a C# component in Rhino 8 that takes a list of points, texts, and images as inputs to draw icons with text on the Rhino canvas. Inspired by these amazing posts:

Custom Sprite Component - Grasshopper Developer - McNeel Forum

Grasshopper Text Dot visibility/overlap - Grasshopper - McNeel Forum

I have attempted to create the component, but I am encountering issues with scaling. Specifically, I want to set the exact size of both the text and the image, but when I zoom in and out, the icons disappear at certain zoom levels. My goal is to dynamically resize the text and the icons based on the Rhino zoom, but I have noticed that after a few zoom adjustments, the component collapses.

As a beginner in C# coding, I am unsure if my approach is correct. It seems that the component keeps recalculating every time the zoom level changes.

Here is the correct display:

When I zoom out, the icon disappears:

And here is the code I have so far:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using Rhino;
using Rhino.Geometry;
using Grasshopper;
using Grasshopper.Kernel;

public class Script_Instance : GH_ScriptInstance
{
    private List<Point3d> points;
    private List<string> texts;
    private List<string> icons;
    private List<Rhino.Geometry.TextDot> textDots = new List<Rhino.Geometry.TextDot>();
    private List<System.Drawing.Bitmap> imageBitmaps = new List<System.Drawing.Bitmap>();
    private List<Point3d> iconPositions = new List<Point3d>();
    private bool eventRegistered = false;

    private void RunScript(List<Point3d> P, List<string> M, List<string> I)
    {
        
        // Ensure parameter names and descriptions are set

        this.Component.Name = "Symbol Display (Custom)";
        this.Component.NickName = "SymCus";
        this.Component.Description = "Symbol display that accepts custom images and optionally draws them in the foreground";

        this.Component.Params.Input[0].Name = "Point";
        this.Component.Params.Input[0].NickName = "P";
        this.Component.Params.Input[0].Description = "Location to add symbol";

        this.Component.Params.Input[1].Name = "Message";
        this.Component.Params.Input[1].NickName = "M";
        this.Component.Params.Input[1].Description = "Message to show in the symbol";

        this.Component.Params.Input[2].Name = "Image";
        this.Component.Params.Input[2].NickName = "I";
        this.Component.Params.Input[2].Description = "Image to show in the symbol";
        
        this.points = P;
        this.texts = M;
        this.icons = I;

        if (this.Component.Hidden)
        {
            ClearDrawings();
        }
        else
        {
            RegisterEvent();
            UpdateTextDotsAndImages();
        }
    }

    private void RegisterEvent()
    {
        if (!eventRegistered)
        {
            Rhino.Display.DisplayPipeline.DrawForeground -= DrawForeground;
            Rhino.Display.DisplayPipeline.DrawForeground += DrawForeground;
            eventRegistered = true;
        }
    }

    private void UpdateTextDotsAndImages()
    {
        // Clear existing elements
        textDots.Clear();
        imageBitmaps.Clear();
        iconPositions.Clear();

        if (points == null || texts == null || icons == null || points.Count == 0 || texts.Count == 0 || icons.Count != points.Count)
            return;

        for (int i = 0; i < points.Count && i < texts.Count; i++)
        {
            var textPoint = new Point3d(points[i].X, points[i].Y, points[i].Z + 2.5);
            textDots.Add(new Rhino.Geometry.TextDot(texts[i], textPoint)
            {
                FontFace = "Calibri",
                FontHeight = 10 // Default; will be adjusted in DrawForeground
            });

            var image = ConvertBase64ToImage(icons[i]);
            if (image != null)
                imageBitmaps.Add(image);

            iconPositions.Add(points[i]);
        }
    }

    private System.Drawing.Bitmap ConvertBase64ToImage(string base64String)
    {
        try
        {
            var byteArray = Convert.FromBase64String(base64String);
            using (var ms = new MemoryStream(byteArray))
            {
                return new System.Drawing.Bitmap(ms);
            }
        }
        catch (Exception ex)
        {
            Rhino.RhinoApp.WriteLine($"Error converting base64 to image: {ex.Message}");
            return null;
        }
    }

    private void DrawForeground(object sender, Rhino.Display.DrawEventArgs e)
    {
        if (textDots.Count == 0 || this.Component.Hidden)
            return;

        if (e.Viewport.Id != Rhino.RhinoDoc.ActiveDoc.Views.ActiveView.ActiveViewportID)
            return;

        var cameraLocation = e.Viewport.CameraLocation;
        const double baseDistance = 100.0; // Adjust base distance for scaling

        for (int i = 0; i < textDots.Count; i++)
        {
            var dot = textDots[i];
            var image = i < imageBitmaps.Count ? imageBitmaps[i] : null;
            var textPoint = dot.Point;
            var iconPoint = i < iconPositions.Count ? iconPositions[i] : Point3d.Unset;

            // Calculate font size based on camera distance
            var distanceToPoint = cameraLocation.DistanceTo(textPoint);
            var scaleFactor = baseDistance / distanceToPoint;
            var fontSize = Math.Max(10.0 * scaleFactor, 1.0);

            // Update and draw the text dot
            dot.FontHeight = (int)fontSize;
            e.Display.DrawDot(dot, Color.FromArgb(255, 255, 204, 77), Color.Black, Color.Black);

            // Draw the image if available
            if (image != null)
            {
                var bitmap = new Rhino.Display.DisplayBitmap(image);
                var drawList = new Rhino.Display.DisplayBitmapDrawList();
                drawList.SetPoints(new List<Point3d> { iconPoint });
                e.Display.DrawSprites(bitmap, drawList, 2, true);
            }
        }
    }

    private void ClearDrawings()
    {
        if (eventRegistered)
        {
            Rhino.Display.DisplayPipeline.DrawForeground -= DrawForeground;
            eventRegistered = false;
        }

        textDots.Clear();
        imageBitmaps.Clear();
        iconPositions.Clear();
    }
}

Any suggestions or insights on how to properly make the component works (or optimised) would be greatly appreciated. Also, it would be great to have all the icons and texts removed if the component is disabled. Thank you! @michaelvollrath @AndersDeleuran

Warning.gh (11.0 KB)

TextDot sizes are already defined in screen-space, not world space, so you shouldn’t have to scale them.

I attached a somewhat modified version of your file hat shows how I would have written this. It’s not actually working, I’m not seeing the bitmap, but I don’t know whether that’s because the sprite drawing api doesn’t work on Mac, or because I did make a mistake somewhere, or because… Test it on your machine, and if the sprites are still invisible, then I made a mistake somewhere.

Main differences are:

  1. I perform class level collection cleanup using the Iteration == 0 condition in the main method. This also allows me to change the inputs from lists to items.
  2. I don’t create sprite collections to be drawn at every draw call, I instead store the Rhino.Display.DisplayBitmap instances in a list and draw them individually.
  3. I use the in-build preview methods on the Script component to draw things.
  4. I disabled depth-testing prior to drawing to make sure my stuff is drawn on top of everything else.

An obvious optimisation which I didn’t write is to re-use already constructed DisplayBitmaps whenever possible. Loading each bitmap individually is probably quite a bad idea if you’re drawing more than a few.

DisplayWarningInViewports.gh (8.4 KB)

1 Like

Works for me in R8.

No such errors with @DavidRutten version.

Hi @Joseph_Oster

Sorry for the human plugin…(do not worry about the other one, it does not do anything in this particular script) I’ve just removed it, so the image has been storaged in a text container. Thank you for your answer @DavidRutten . Maybe I did not make myself clear. What I want is to set the size of the text and icon inside rhino canvas in a way that when zooming in and out, they changes since the text are referred to a geometry inside rhino.

This is the effect I want to get but including also the text:

Thanks in advance!

Warning_1.gh (24.2 KB)

In that case maybe change the drawing loop to

for (int i = 0; i < _dots.Count; i++)
{
  var dot = _dots[i];
  var point = dot.Point;
  args.Viewport.GetWorldToScreenScale(point, out var pixPerUnit);

  var size = Math.Min(Math.Max(pixPerUnit, 1), 30);
  if (size <= 6) continue;

  dot.FontHeight = (int)size;
  args.Display.DrawDot(dot, fill, edge, edge);

  var icon = _icons[i];
  if (icon != null)
  {
    var xy = args.Viewport.WorldToClient(point);
    var w = 5 * (float)size;
    var h = 5 * (float)size;
    xy.Y += 0.5f * h;
    args.Display.DrawSprite(icon, xy, w, h);
  }
}

I’m short circuiting the loop if the text dot would be so small that it becomes unreadable, and I limit the upper size of the dots. You may want to pick different constants here.

1 Like

That is awesome! It perfectly works! Thank you very much for your help! :slight_smile:

Only one last issue, is it possible to bring everything always to the front? Is that something related to the DrawForeground method? @DavidRutten