Dynamic Redraw & Retrieving User Values Prior to Hitting Enter

Hi All,

Quick question that is likely deeper than I yet know…

I’m working on a little command which simulates divide while I learn Rhino Common and C# - since I know how the command is supposed to work it’s been helpful. My little twist is id like to give the user the ability to specify a division which starts at a point and radiates outward to the ends of the curve (a pretty common action in curtainwall layouts). The goal is to allow the user to specify a startpoint and then specify length of the divisions (for now).

However, studying the divide command closely I noticed that when entering length the user can see a real time preview of where the points are going to be without having to press the enter key at all! This is a really nice quality of life feature and I definitely want to add it but I’ve been struggling to figure out how to pass that value dynamically.

Best I’ve gotten so far is this -

    public class DivideOut : Command
    {
        public override string EnglishName => "DivideOut";  // Command name

        protected override Result RunCommand(RhinoDoc doc, RunMode mode)
        {
            // Filter for selecting a curve
            const ObjectType filter = ObjectType.Curve;
            ObjRef objRef;

            // Ask the user to select a curve
            var rc = RhinoGet.GetOneObject("Select a curve to divide", false, filter, out objRef);
            if (rc != Result.Success || objRef == null)
                return rc;

            var curve = objRef.Curve();
            if (curve == null || curve.IsShort(RhinoMath.ZeroTolerance))
                return Result.Failure;  // If curve is invalid, return Result.Failure

            // Ask the user to click on a point for the division start
            Point3d startPoint;
            rc = RhinoGet.GetPoint("Select the start point for the division", false, out startPoint);
            if (rc != Result.Success)
                return rc;

            // Find the parameter corresponding to the start point on the curve
            double startParam;
            var success = curve.ClosestPoint(startPoint, out startParam);
            if (!success)
                return Result.Failure;

            // Initial placeholder for the segment length
            double segmentLength = 0;
            double totalLength = curve.GetLength();

            // Check that the total length of the curve is sufficient for division
            if (totalLength < 0.001)
            {
                RhinoApp.WriteLine("The curve is too short to divide.");
                return Result.Failure;
            }

            // Store the preview points as Rhino objects for later deletion
            List<RhinoObject> previewObjects = new List<RhinoObject>();

            // We will use a GetNumber instance and loop continuously to emulate real-time input
            var getNumber = new GetNumber();
            getNumber.SetCommandPrompt("Enter the length of each segment");
            getNumber.SetLowerLimit(0.001, true);  // Set a lower limit for the distance
            getNumber.AcceptNumber(true, false);  // Enable dynamic input (without pressing Enter)

            // Initial dynamic input loop (waits for user input continuously)
            bool loopRunning = true;
            while (loopRunning)
            {
                // Get the real-time input from the user
                GetResult result = getNumber.Get();

                if (result == GetResult.Cancel)
                {
                    loopRunning = false;  // Exit loop if the user cancels
                    continue;  // Move on to finish
                }

                // Ensure the number is valid
                if (getNumber.Number() <= 0)
                {
                    RhinoApp.WriteLine("Segment length must be greater than zero.");
                    continue;  // Keep asking until valid input is provided
                }

                // Update the segment length dynamically as user types
                segmentLength = getNumber.Number();

                // Delete previously drawn preview points
                foreach (var obj in previewObjects)
                {
                    doc.Objects.Delete(obj, true);  // Delete each preview point
                }
                previewObjects.Clear();  // Clear the list of preview points

                // Calculate how many segments can fit along the curve
                int segmentCount = (int)(totalLength / segmentLength);

                previewObjects.Clear();  // Reset preview points

                // Divide the curve in the forward direction (from start point)
                for (int i = 0; i <= segmentCount; i++)
                {
                    double t = startParam + i * segmentLength / totalLength * curve.GetLength();
                    if (t > curve.GetLength()) break;  // Ensure we do not exceed the curve length
                    Point3d point = curve.PointAt(t);
                    var pointObj = doc.Objects.AddPoint(point);  // Add point to the document
                    previewObjects.Add(doc.Objects.Find(pointObj));  // Store the RhinoObject
                }

                // Divide the curve in the backward direction (from start point)
                for (int i = 1; i <= segmentCount; i++)  // Start from 1 to avoid duplicating the start point
                {
                    double t = startParam - i * segmentLength / totalLength * curve.GetLength();
                    if (t < 0) break;
                    Point3d point = curve.PointAt(t);
                    var pointObj = doc.Objects.AddPoint(point);
                    previewObjects.Add(doc.Objects.Find(pointObj));  // Store the RhinoObject
                }

                // Redraw the view to show the points dynamically
                doc.Views.Redraw();
            }

            // Finalize the division and stop
            return Result.Success;  // Command completed successfully
        }
    }

But I have to hit enter once to preview and again to confirm the selection. It gives the user the same control but is not nearly as intuitive. I’d like to keep the user experience as close as possible to the vanilla divide command rather than expecting someone to learn a different logic for the sake of this practice command!

https://developer.rhino3d.com/api/rhinocommon/rhino.input.custom.getbaseclass/acceptnumber this seems like a step in the right direction but I’m not sure how to implement beyond as I have.

Apologies if this has been answered before and bear with me!

Sounds like you might needed to setup your own display pipeline. And then have it draw the points within your update loop while the command is running. You want to read up on Display Conduits:

1 Like

Just adding to @jstevenson advice, but you could check out this conversation:

https://developer.rhino3d.com/api/rhinocommon/rhino.input.custom.getpoint#methods

DynamicDraw on GetPoint allows you to do stuff while in the limbo state before the user ends the command.

1 Like

Hi @Rosenberg_Noah,

Here is a sample, albeit rough, for you to review.

TestDivideOutCommand.cs (5.9 KB)

Let me know if you have any questions.

– Dale

2 Likes

Hi Jason, super new to display conduits and I’m trying to understand how this would assist other than allowing for drawing the points in an interesting/custom way? My code allows for the points to be previewed in the viewport so that isn’t an issue, only that i want the preview to update when i type a number WITHOUT having to hit enter. If you play with the default rhino divide command it has this functionality - where a user can type 5 and see 5 points, and then hit backspace and type 4 to see 4 and then hit enter to confirm. That’s pretty much all I’m after, is this a display conduit thing or just a logic issue I’m missing?

Thanks for getting back and for weighing in - apologies for my ignorance!

Any command that is creating geometry, that also wants to “show” that geometry without affecting the current document, will need to use a display conduit to draw the objects while the command is running it’s input loop.

Because the points won’t actually be there in the document, you have to draw them yourself. You will have to follow @dale excellent sample.

You should be able to use his example as your base, and expand on it to add your existing logic.

Hi @Rosenberg_Noah,

This fanciness is limited to core Rhino commands and it not available to C++ or RhinoCommon plug-ins, sorry.

– Dale

Thank you for this! This has been a really great resource for understanding the syntax for display conduit. I’m still a little perplexed by how they work at a high level and what an ideal use case would be other than fancy graphical customization?

Bummer! However, I’m pretty glad to hear this considering the amount of time I spent trying to figure out how to get this to work :wink:. Out of curiosity could you explain why - sorry if this is secret stuff!

Hi Jason, still wrapping my head around Display conduits, but thanks for getting back to me! Is this different than something like OnDynamicDraw? I’ve been using stuff like the below to draw most of my previews. (This example is a 2d door graphic with ADA Clearances) Is the difference between these functions that the display conduits are purely graphical overlay onto the viewport? Just trying to understand the use case for different approaches.

   private void FinalPhaseDynamicDraw(GetPoint sender, GetPointDrawEventArgs args, Point3d DoorPoint, Point3d RotationPoint, double DoorWidth, double DoorThickness, double WallThickness, bool Flip, bool PullFront, bool PushFront, bool PullLatch, bool PushLatch, bool PullHinge, bool PushHinge)
    {
        args.Display.DrawDot(DoorPoint, "Placement Point", Color.DarkBlue, Color.White); // Draw a dot for DoorPoint
        args.Display.DrawDot(RotationPoint, "Rotation Point", Color.DarkRed, Color.White); // Draw a dot for RotationPoint

        // Create FullDrawing with the updated DoorPoint and PreviewRotationPoint
        List<Curve> FullDrawing = DrawEverything(DoorPoint, RotationPoint, DoorWidth, DoorThickness, WallThickness, Flip, PullFront, PushFront, PullLatch, PushLatch, PullHinge, PushHinge);

        // Loop through each curve in FullDrawing and draw it
        foreach (Curve curve in FullDrawing)
        {
            args.Display.DrawCurve(curve, Color.White); // Draw the curve in red for visibility
        }
    }`
1 Like

To be honest, I’m not familiar with DynamicDraw(). But I suspect after viewing some other posts on here, that the DynamicDraw is limited in what you can do, compared to writing your own display conduit.

We’ve only ever used display conduits in our software.

1 Like

tl:dr DynamicDraw is a shortcut for a DisplayConduit, but only exists during GetPoint. DisplayConduit is more complicated but lets you display stuff whenever you want and gives you some other flexibility.

Hey @Rosenberg_Noah Its kind of covered in the link @jstevenson posted on Display Conduits, but Rhino is always displaying stuffing in the “DisplayPipeline” . When you add your own DisplayConduit and override the event handlers, you’re asking Rhino “Hey can you display this stuff as well?”, and Rhino will add it to the things it will display. With a conduit you get a lot of flexibility with how and when things are drawn, whereas DynamicDraw() only tells Rhino to draw whatever it is you want drawn for the duration of GetPoint.

To relate it back to what you were trying to do with the curve division, DynamicDraw lets you see the preview during the command, whereas DisplayConduit could always preview the curve division – a little like how you can toggle the Grasshopper preview on and off whenever you want.

Which one to use comes down to how you want to use whatever you’re making – if the user only needs to preview the line divisions once when they’re deciding the division length, then DynamicDraw could be all you need. But if you catch yourself thinking “Oh it would handy if I could preview the divisions for all these curves at once and have them update to a number input” then using DisplayConduit could be better.

Likewise for your FinalPhaseDynamicDraw() instead of relying on GetPoint to be able to preview your ADA clearances, you could just have a command that turns your clearance preview off and on (DisplayConduits have a enable bool). It can get fun as you can hook it up so someone could turn on/off the various things you want to draw i.e. toggle DoorThickness on or off etc.

However it can be tricky to implement DisplayConduits correctly – I’ve spent a lot of time fiddling with things like calculating bounding boxes, z-depth and ensuring things are being drawn efficiently. Is a lot of fun when it works though.

Apologies @dale if I’ve butchered this explanation…