Subscribe to an object's transformation (event handler)

I’m sure this has been answered by someone else before, and apologies if this is a bit of a novice question, but I am trying to create a system which uses text dots to report positions of an attributed curve and uses record history to capture changes to the curve to inform changes to the text within. Here’s my command class thus far! - Bear in mind I’m a bit new to the world of Rhino Common, but dug for quite a while and couldn’t seem to find a method that would work similar to the way that Rhinodoc.SelectObjects works but for object transformations?

Right now the command lets the user place a point and attributes a text dot with elevational data for it (z location). It writes to history and allows moving the point to update the text and location of the text dot. However, if the text dot is moved independently of the point, it loses association. I’m using a dictionary to tie the GUIDs of the points and the dots together. The intended behavior is that moving either object would move the other! Currently, I’m cheating and updating the text dot when the point object is selected!

Any help or guidance is most appreciated. ChatGPT has been my instructor and confidant working on this so efforts to put explanations in layman’s terms are greatly appreciated!

    public class ElevationTarget : Command
    {
        static ElevationTarget g_command_instance;

        private const int HISTORY_VERSION = 20131107;

        // Dictionary to store each point and its corresponding TextDot ID
        private Dictionary<Guid, Guid> pointTextDotMap = new Dictionary<Guid, Guid>();

        // Global offset for elevation
        private static double globalOffset = 0.0;

        public ElevationTarget()
        {
            g_command_instance = this;
        }

        public static ElevationTarget Instance => g_command_instance;

        public override string EnglishName => "ElevationTarget";

        protected override Result RunCommand(RhinoDoc doc, RunMode mode)
        {
            // Ask the user if they want to modify the global offset
            Rhino.Input.Custom.GetOption getOption = new Rhino.Input.Custom.GetOption();
            getOption.SetCommandPrompt("Select an option");
            Rhino.Input.Custom.OptionDouble globalOffsetOption = new Rhino.Input.Custom.OptionDouble(globalOffset);
            getOption.AddOptionDouble("GlobalOffset", ref globalOffsetOption);

            // Let the user choose whether they want to modify the global offset
            var getOptionResult = getOption.Get();
            if (getOptionResult == Rhino.Input.GetResult.Option)
            {
                globalOffset = globalOffsetOption.CurrentValue;
                UpdateAllTextDots(doc); // Update all TextDots immediately to reflect the new global offset
            }

            // Let the user pick a point interactively in the viewport
            Point3d pickedPoint;
            var getPointResult = Rhino.Input.RhinoGet.GetPoint("Click to place a new point", false, out pickedPoint);

            // If the user cancels or fails to pick a point, exit the command
            if (getPointResult != Rhino.Commands.Result.Success)
                return getPointResult;

            // Create a new point in the document at the picked location (Point3d used directly)
            Point point = new Point(pickedPoint);  // This is valid since Point3d is used as input
            Guid pointId = doc.Objects.AddPoint(pickedPoint);  // Add the point to the document using Point3d

            // Update the elevation display as a TextDot for this new point
            UpdateTextDot(doc, pickedPoint, pointId); // Use Point3d here as the argument for TextDot

            // Subscribe to the SelectObjects event to monitor selection changes
            RhinoDoc.SelectObjects += OnSelectObjects;

            // Subscribe to the DeleteRhinoObject event to handle manual deletion of TextDots
            RhinoDoc.DeleteRhinoObject += OnDeleteRhinoObject;

            doc.Views.Redraw();

            return Rhino.Commands.Result.Success;
        }

        protected override bool ReplayHistory(Rhino.DocObjects.ReplayHistoryData replay)
        {
            Rhino.DocObjects.ObjRef objref = null;

            if (!ReadHistory(replay, ref objref))
                return false;

            Point3d pointLocation = objref.Point().Location;
            if (pointLocation == Point3d.Unset)
                return false;

            // Update the elevation display as a TextDot
            UpdateTextDot(RhinoDoc.ActiveDoc, pointLocation, objref.ObjectId);

            return true;
        }

        private bool ReadHistory(Rhino.DocObjects.ReplayHistoryData replay, ref Rhino.DocObjects.ObjRef objref)
        {
            if (HISTORY_VERSION != replay.HistoryVersion)
                return false;

            objref = replay.GetRhinoObjRef(0);
            if (objref == null)
                return false;

            return true;
        }

        private bool WriteHistory(Rhino.DocObjects.HistoryRecord history, Rhino.DocObjects.ObjRef objref)
        {
            if (!history.SetObjRef(0, objref))
                return false;

            return true;
        }

        private void UpdateTextDot(RhinoDoc doc, Point3d pointLocation, Guid pointId)
        {
            // Get the current Z-elevation of the point
            double elevation = pointLocation.Z;

            // Convert the Z-elevation to the current document's units
            Rhino.UnitSystem docUnitSystem = doc.ModelUnitSystem;
            double elevationInUnits = UnitConverter.ConvertToMeters(elevation, docUnitSystem);

            // Apply the global offset
            double adjustedElevation = elevationInUnits + globalOffset;

            // Create the text for the TextDot with the adjusted elevation and the global offset
            string text = $"Elevation: {adjustedElevation:F2} {docUnitSystem.ToString()}\nGlobal Offset: {globalOffset:F2} {docUnitSystem.ToString()}";

            // Get the location of the point (for the TextDot)
            Point3d textDotLocation = pointLocation;

            // If there's already a TextDot for this point, delete the old one
            if (pointTextDotMap.ContainsKey(pointId) && pointTextDotMap[pointId] != Guid.Empty)
            {
                Guid oldTextDotId = pointTextDotMap[pointId];

                // Attempt to delete the old TextDot object
                var oldTextDotObj = doc.Objects.Find(oldTextDotId);
                if (oldTextDotObj != null)
                {
                    doc.Objects.Delete(oldTextDotId, true);
                }

                // Ensure we update the map after deletion
                pointTextDotMap[pointId] = Guid.Empty;
            }

            // Create a new TextDot for this point, showing the adjusted elevation and global offset
            TextDot textDot = new TextDot(text, textDotLocation);

            // Create an ObjectAttributes object to assign attributes to the TextDot
            var objectAttributes = new ObjectAttributes();

            // Check if the "LEVELS" layer exists
            Layer levelsLayer = doc.Layers.FindName("LEVELS");

            if (levelsLayer != null)
            {
                // If "LEVELS" layer exists, set the TextDot to that layer using ObjectAttributes
                objectAttributes.LayerIndex = levelsLayer.Index;
            }
            else
            {
                // Otherwise, use the current layer of the point
                var pointObject = doc.Objects.Find(pointId);
                if (pointObject != null)
                {
                    objectAttributes.LayerIndex = pointObject.Attributes.LayerIndex;
                }
            }

            // Add the new TextDot to the document with the assigned ObjectAttributes and update the dictionary with its ID
            Guid textDotId = doc.Objects.AddTextDot(textDot, objectAttributes);
            pointTextDotMap[pointId] = textDotId; // Map the point ID to the new TextDot ID

            // Ensure that the scene is updated
            doc.Views.Redraw();
        }

        private void UpdateAllTextDots(RhinoDoc doc)
        {
            // Collect the keys (point IDs) in a separate list to avoid modifying the dictionary during enumeration
            List<Guid> pointIds = pointTextDotMap.Keys.ToList();

            // Loop through the collected point IDs
            foreach (var pointId in pointIds)
            {
                // Find the point object by its ID
                Rhino.DocObjects.RhinoObject pointObject = doc.Objects.Find(pointId);
                if (pointObject != null && pointObject is Rhino.DocObjects.PointObject pointObject2)
                {
                    Point point = pointObject2.Geometry as Point;
                    if (point != null)
                    {
                        UpdateTextDot(doc, point.Location, pointId);  // Pass Point3d here
                    }
                }
            }
        }

        private void OnSelectObjects(object sender, Rhino.DocObjects.RhinoObjectSelectionEventArgs e)
        {
            RhinoDoc doc = RhinoDoc.ActiveDoc;

            // Get the selected objects
            var selectedObjects = doc.Objects.GetSelectedObjects(false, false);

            // Iterate over all selected objects
            foreach (var selectedObject in selectedObjects)
            {
                // Check if the selected object is a point and is in our dictionary
                if (selectedObject is Rhino.DocObjects.PointObject pointObject && pointTextDotMap.ContainsKey(pointObject.Id))
                {
                    // Update the corresponding TextDot for this point
                    Point point = pointObject.Geometry as Point;
                    if (point != null)
                    {
                        UpdateTextDot(doc, point.Location, pointObject.Id);  // Use Point3d location
                    }
                }
            }
        }

        // Handle manual deletion of TextDots
        private void OnDeleteRhinoObject(object sender, Rhino.DocObjects.RhinoObjectEventArgs e)
        {
            // If the deleted object is a TextDot, we need to remove it from the dictionary
            if (e.TheObject is TextDotObject textDot)
            {
                // Use the ObjectId to get the GUID of the TextDot
                Guid textDotId = e.TheObject.Id;

                // Find the point ID associated with this TextDot and remove it from the dictionary
                var entry = pointTextDotMap.FirstOrDefault(kv => kv.Value == textDotId);
                if (entry.Key != Guid.Empty)
                {
                    // Remove the mapping from the dictionary
                    pointTextDotMap.Remove(entry.Key);
                }

                // Perform any additional cleanup if necessary
            }
        }
    }
}

For this kind of thing I find the SampleCSEventWatcher developer sample really handy. If you clone that repo and build it and then run the SampleCsEventWatcher command, it’ll list all of the events to the command line as you do an action. For example, for dragging a curve with the gumball, I see:

Basically, OnBeforeTransformObjects fires with the transform matrix, and then the curve geometry is replaced with the transformed version in ReplaceRhinoObject.

5 Likes

Hi @Rosenberg_Noah,

Please review the following sample - no ChatGPT. :wink:

Let me know if you have any questions.

TestElevationTargetCommand.cs (6.1 KB)

– Dale

2 Likes

Absolutely what I was looking for! Although I still need to dig into understanding the reading and writing methods for history the organization here makes it so much more clear how record history actually works - excited to learn!

Thank you for this Dale (and for being lightning fast!), I’ll bug you further once my literacy is better… (Sorry in advance!)

Hi Dan, I was looking at this sample actually, but I’m so green that I don’t think I fully understood what all of the methods did. Is the, public void Enable(bool enable) method showing EVERY possible event handler or is that list just a sample? Would OnBeforeTransformObjects be the one registering the drag? and is OnSelectObjects; the one being invoked prior to that? Thanks for getting back to me and sharing!