Replacing Geometry Without Breaking History

I am trying to write a command which takes an object as input and creates a piece of geometry that is “attached” to that object via history. The command also allows the user to reselect the previously created geometry and modify it using new parameters. I am running into a problem with the latter half of my requirement.

When initially creating geometry I use RhinoDoc.Objects.Add to add it to the document with a history record.

When modifying the previously created geometry I use RhinoDoc.Objects.Replace so that my object retains it’s original ID, however, this breaks my previously created history.

Animation without SetCopyHistoryOnReplace

I have found that if add a call to RhinoObject.SetCopyHistoryOnReplace(true) before calling RhinoDoc.Objects.Replace that my history will not break, however, it does introduce a new unintended behavior which I can not explain.

Animation with SetCopyHistoryOnReplace

By using RhinoObject.SetCopyHistoryOnReplace(true) prior to RhinoDoc.Objects.Replace my history remains but now it seems as though the connection is bi-directional! Meaning that where as if I grabbed my geometry and moved it before I could expect that to break history, now it does not break history and in fact it snaps back to it’s original location.

I’m looking for some guidance here on how to properly use RhinoDoc.Objects.Replace without breaking history. Ideally the replace method would take a history record or something that I could update with new parameters as well.

1 Like

For your reference… here is my full test command to demonstrate my problem.

using Rhino;
using Rhino.Commands;
using Rhino.DocObjects;
using Rhino.Geometry;
using Rhino.Input;
using Rhino.Input.Custom;

namespace TestPlugin.Core.Commands
{
    [CommandStyle(Style.Hidden)]
    public class WhatTheHistoryIsGoingOnCommand : Command
    {
        private const int HistoryVersion = 20230606;

        public WhatTheHistoryIsGoingOnCommand()
        {
            Instance = this;
        }

        public static WhatTheHistoryIsGoingOnCommand Instance
        {
            get; private set;
        }

        public override string EnglishName => $"testWhatTheHistoryIsGoingOnCommand";

        protected override bool ReplayHistory(ReplayHistoryData replayData)
        {
            if (!TryReadHistory(replayData, out ObjRef requiredObjRef, out double buffer))
                return false;

            // Create output
            Brep outputBrep = CreateOutput(requiredObjRef, buffer);

            if (outputBrep == null)
                return false;

            // Update
            replayData.Results[0].UpdateToBrep(outputBrep, null);

            return true;
        }

        protected override Result RunCommand(RhinoDoc doc, RunMode mode)
        {
            // Get required
            ObjRef requiredObjRef = GetRequired(doc);

            if (requiredObjRef == null)
                return Result.Failure;

            // Get optional
            ObjRef existingObjRef = GetOptionalExisting(doc);

            // Get buffer
            double buffer = GetBuffer();

            if (!RhinoMath.IsValidDouble(buffer))
                return Result.Failure;

            // Create output
            Brep outputBrep = CreateOutput(requiredObjRef, buffer);

            if (outputBrep == null)
                return Result.Failure;

            // Add to document
            if (existingObjRef == null)
            {
                if (TryWriteHistory(requiredObjRef, buffer, out HistoryRecord history))
                    doc.Objects.Add(outputBrep, null, history, false);
                else
                    doc.Objects.Add(outputBrep, null);
            }
            else
            {
                RhinoObject existingRhinoObject = existingObjRef.Object();
                existingRhinoObject.SetCopyHistoryOnReplace(true);

                doc.Objects.Replace(existingObjRef, outputBrep);
            }

            doc.Views.Redraw();

            return Result.Success;
        }

        private Brep CreateOutput(ObjRef requiredObjRef, double buffer)
        {
            Brep requiredBrep = requiredObjRef.Brep();

            if (requiredBrep == null)
                return null;

            BoundingBox requiredBrepBoundingBox = requiredBrep.GetBoundingBox(true);
            requiredBrepBoundingBox.Inflate(buffer);

            return requiredBrepBoundingBox.ToBrep();
        }

        private double GetBuffer()
        {
            // Get
            var get = new GetNumber();
            get.SetCommandPrompt("Enter buffer");
            get.SetDefaultNumber(0.0);
            get.SetLowerLimit(0.0, false);

            // Check get result
            GetResult getResult = get.Get();

            if (getResult != GetResult.Number)
                return RhinoMath.UnsetValue;

            // Check command result
            Result commandResult = get.CommandResult();

            if (commandResult != Result.Success)
                return RhinoMath.UnsetValue;

            // Number
            return get.Number();
        }

        private ObjRef GetOptionalExisting(RhinoDoc doc)
        {
            // Get
            var get = new GetObject
            {
                GeometryFilter = ObjectType.Brep,
                GeometryAttributeFilter = GeometryAttributeFilter.ClosedPolysrf
            };

            get.AcceptNothing(true);
            get.EnablePostSelect(true);
            get.EnablePreSelect(false, true);
            get.SetCommandPrompt($"Select existing bounding box or press ENTER to skip");

            // Check get result
            GetResult getResult = get.Get();

            if (getResult != GetResult.Object)
                return null;

            // Check command result
            Result commandResult = get.CommandResult();

            if (commandResult != Result.Success)
                return null;

            // Object
            ObjRef objRef = get.Object(0);

            //// Handle selection
            //if (!get.ObjectsWerePreselected)
            //{
            //    doc.Objects.Select(objRef, false);

            //    doc.Views.Redraw();
            //}

            return objRef;
        }

        private ObjRef GetRequired(RhinoDoc doc)
        {
            // Get
            var get = new GetObject
            {
                GeometryFilter = ObjectType.Brep
            };

            get.EnablePostSelect(true);
            get.EnablePreSelect(false, true);
            get.SetCommandPrompt($"Select object to bounding box");

            // Check get result
            GetResult getResult = get.Get();

            if (getResult != GetResult.Object)
                return null;

            // Check command result
            Result commandResult = get.CommandResult();

            if (commandResult != Result.Success)
                return null;

            // Object
            ObjRef objRef = get.Object(0);

            //// Handle selection
            //if (!get.ObjectsWerePreselected)
            //{
            //    doc.Objects.Select(objRef, false);

            //    doc.Views.Redraw();
            //}

            return objRef;
        }

        private bool TryReadHistory(ReplayHistoryData replay, out ObjRef requiredObjRef, out double buffer)
        {
            // Default output
            requiredObjRef = null;

            buffer = 0.0;

            // Check history version
            if (HistoryVersion != replay.HistoryVersion)
                return false;

            // Read
            requiredObjRef = replay.GetRhinoObjRef(0);

            if (requiredObjRef == null)
                return false;

            if (!replay.TryGetDouble(1, out buffer))
                return false;

            return true;
        }

        private bool TryWriteHistory(ObjRef requiredObjRef, double buffer, out HistoryRecord history)
        {
            // Create record
            history = new HistoryRecord(this, HistoryVersion);

            // Write
            if (!history.SetObjRef(0, requiredObjRef))
                return false;

            if (!history.SetDouble(1, buffer))
                return false;

            return true;
        }
    }
}
3 Likes

Hi @Mike_Muller,

If I understand correctly, you want to do something like the BlendCrv and BlendSrf commands, which both have Edit options. This capability is not currently available in RhinoCommon. You should be able to do this in C++, however.

– Dale

My example is complex so let me break down the part of this that I am most concerned with:.

I create the cube and add it to the document using RhinoDoc.Objects.Add and supplying a history record which references the sphere. I can move my sphere and the cube follows like it’s supposed to. If I move my cube this will break history as I expect.

Next I run my command again and recreate my cube and replace the original cube by calling RhinoObject.SetCopyHistoryOnReplace(true) on the original cube and then RhinoDoc.Objects.Replace passing the new cube geometry. I can move my sphere and cube follows like it’s supposed to. If I try to move my cube now, history does not break and instead the cube snaps back to its original position because the Command.ReplayHistory method is executing.

Why would moving the cube at this point trigger history to replay instead of break?

Hi @Mike_Muller,

You might try this pattern:

objref.Object()?.SetCopyHistoryOnReplace(true);
doc.Objects.Replace(objref, geometry);
objref.Object()?.SetCopyHistoryOnReplace(false);

If this doesn’t help, I’ll need a sample…

– Dale

3 Likes

So far so good! I don’t understand why this fixes my issue but it does seem to fix it.

I’m really trying to better understand how the Rhino history system works. Can you elaborate at all on why it’s necessary to call RhinoObject.SetCopyHistoryOnReplace(false) and why that strange behavior was happening when I was missing that call?