Thursday, November 17, 2022

Loop Operation with SendStringToExecute()

While programming an AutoCAD plugin, there are many occasions we use Document.SendStringToExecute() method to existing command, which can be built-in commands or custom commands (including lisp-defined commands). 

Using SendStringToExecute() could save a lot of programming effort for particular AutoCAD operations when we know there are existing commands/lisp routines that does exactly the work, especially when the existing commands/lisp routines do some work that we programmers cannot easily do with our own code, and in many case, why reinventing the wheel anyway?

However, due to its "async" nature, it cannot be used in middle of a chunk of our code and expect the code following it can operate on the result produced from the SendStringToExecute() call. For example, if we need to call SendStringToExecute() repeatedly as loop in a custom operation of our code, following code simply does not work:

private void DoRepeatedWork(IEnumerable<ObjectId> entIds)
{
    var dwg = Application.DocumentManager.MdiActiveDocument;
    foreach (var entId in entIds)
    {
        // set PickFirst selections
 
        dwg.SendStringToExecute("SomeCommand\n"falsefalsefalse);
 
        // Do something with the result of SendStringToExecute()
 
    }
}

The key to be able to repeatedly call SendStringToExecute() is to know when the command/lisp routine executed by SendStringToExecute() ends. Fortunately, in the .NET API, there are Document.CommandEnded/Cancelled and Document.LispEnded/Cancelled events for us to handle, so that we know when command/lisp routine execution ends, thus, we can repeatedly call SendStringToExecute() in a loop.

Following code example shows how to use SendStringToExecute() to run an command repeatedly against a collection of entities, one at a time. I created a command "ThirdPartyCmd" to mimic a third party command, which we really do not know (or do not need to know) how it does its work inside. But for the sake to show the "third party" command's effect, I let the command only accept one Line entity as PickFirst selection and rotate it, change its color.

#region Handle CommandEnded for looping
 
private ObjectId[] _selectedLines = null;
private int _currentIndex = 0;
 
private Document _thisDwg = null;
 
[CommandMethod("CmdLoop", CommandFlags.Session)]
public void RunLoopingCommand()
{
    _thisDwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = _thisDwg.Editor;
 
    // Select target entities when the command loop begins
    if (_selectedLines == null)
    {
        var filter = new SelectionFilter(
            new[] { new TypedValue((int)DxfCode.Start, "LINE") });
        var res = ed.GetSelection(filter);
        if (res.Status == PromptStatus.OK)
        {
            _selectedLines = res.Value.GetObjectIds();
        }
        else
        {
            return;
        }
 
        _currentIndex = 0;
    }
    
    // if necessary, do something with the 
    // entity that was processed by SendStringToExecute()
    if (_currentIndex>0)
    {
        //DoSomething(_selectedLines[_currentIndex-1]);
    }
 
    _thisDwg.Editor.SetImpliedSelection(new[] { _selectedLines[_currentIndex] });
    _thisDwg.CommandEnded += ThirdPartyCmd_Ended;
    _thisDwg.SendStringToExecute("ThirdPartyCmd\n"falsefalsefalse);
}
 
private void ThirdPartyCmd_Ended(object sender, CommandEventArgs e)
{
    if (e.GlobalCommandName.ToUpper() != "THIRDPARTYCMD"return;
 
    _thisDwg.CommandEnded -= ThirdPartyCmd_Ended;
 
    _thisDwg.Editor.WriteMessage(
        $"\nLine being processed by ThirdPartyCmd: {_selectedLines[_currentIndex]}");
 
    // increment index
    _currentIndex++;
    _thisDwg.Editor.WriteMessage(
        $"\nThirdPartyCmd execution count: {_currentIndex}");
 
    // End the loop after last target entity is done.
    if (_currentIndex >= _selectedLines.Length)
    {
        _selectedLines= null;
        return;
    }
 
    //Back to original command for another loop
    _thisDwg.SendStringToExecute("CmdLoop\n"truefalsetrue);
}
 
[CommandMethod("ThirdPartyCmd", CommandFlags.UsePickSet)]
public void RotateEntity()
{
    var dwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = dwg.Editor;
 
    var res = ed.SelectImplied();
    
    ed.WriteMessage("\nRunning third party command...,");
    if (res.Status == PromptStatus.OK)
    {
        ed.WriteMessage($"\nProcessing selected entity: {res.Value[0].ObjectId}");
 
        using(var tran=dwg.TransactionManager.StartTransaction())
        {
            var line = (Line)tran.GetObject(res.Value[0].ObjectId, OpenMode.ForWrite);
            line.ColorIndex = new Random().Next(0, 255);
            line.TransformBy(Matrix3d.Rotation(
                Math.PI / 2.0, Vector3d.ZAxis, line.StartPoint));
            tran.Commit();
        }
    }
    
    ed.WriteMessage("\ndone!");
}
 
#endregion

The code is quite self-explanatory. See this video clip see the code running result:


This post is actually inspired by this discussion in the .NET forum. The original poster want to execute command "TxtExp" (exploding text, from Express Tools) repeatedly against a selection of text entities, one at a time, and between the "TxtExp" calls, he/she also want to do something with the exploding-generated entities. He/she also referred an article posted by famous Kean Walmsley about using "TxtExp". "TxtExp" is a lisp command from the Express Tool suite. While it is certainly possible to write our own code to do the same text exploding work, but it would be quite complicated. If we could just use it in our code to achieve our goal, we can save a lot of time/effort. So, I gave it a try, use the same approach as above code shows. 

The difference is that handing CommandEnded event is changed to handing LispEnded. Also, since we know the "TxtExp" (one can study the lisp routine "txtexp.lsp" in "Express" folder for some inside of this command) command generating a bunch of geometry entities from the selected entities, adding them into database, and then erasing the text, we can somehow collect these "exploded" geometry entities between "TxtExp" calls and do something as needed. Again, following code are quite easy to follow, so no extra explanation is needed:

#region Handle LispEnded for looping
 
        private ObjectId[] _selectedTexts = null;
        private List<ObjectId> _explodedEntities = null;
        private int _index = 0;
        private Document _dwg = null;
 
        [CommandMethod("ExplodeTexts")]
        public void ExplodeTextsToGeometryEntities()
        {
            _dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = _dwg.Editor;
 
            if (_selectedTexts == null)
            {
                var filter = new SelectionFilter(new TypedValue[]
                {
                new TypedValue((int)DxfCode.Start, "TEXT")
                });
 
                var res = ed.GetSelection(filter);
                if (res.Status != PromptStatus.OK) return;
 
                _selectedTexts = res.Value.GetObjectIds();
 
                _index= 0;
            }
 
            if (_index>0)
            {
                UpdateExplodedEntities();
            }
 
            _dwg.LispEnded += Lisp_Ended;
            _dwg.Database.ObjectAppended += Entity_Appended;
 
            _explodedEntities = new List<ObjectId>();
            
            ed.SetImpliedSelection(new[] { _selectedTexts[_index] });
            _dwg.SendStringToExecute("TXTEXP\n"falsefalsefalse);
        }
 
        private void Lisp_Ended(object sender, EventArgs e)
        {
            ObjectId id = _selectedTexts[_index];
            if (id.IsErased)
            {
                _dwg.LispEnded -= Lisp_Ended;
                _dwg.Database.ObjectAppended -= Entity_Appended;
 
                _dwg.Editor.WriteMessage($"\nExploding text count: {_index + 1}");
                _index++;
 
                // end looping
                if (_index >= _selectedTexts.Length)
                {
                    _selectedTexts = null;
                    return;
                }
 
                // Do next loop
                _dwg.SendStringToExecute("ExplodeTexts\n"falsefalsefalse);
            }
        }
 
        private void Entity_Appended(object sender, ObjectEventArgs e)
        {
            if (e.DBObject is Entity)
            {
                _explodedEntities.Add(e.DBObject.ObjectId);
            }
        }
 
        private void UpdateExplodedEntities()
        {
            var color = new Random().Next(0, 255);
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                foreach (var entId in _explodedEntities)
                {
                    if (!entId.IsErased)
                    {
                        var ent = (Entity)tran.GetObject(entId, OpenMode.ForWrite);
                        ent.ColorIndex = color;
                    }
                }
                tran.Commit();
            }
        }
 
        #endregion


See the video clip below showing the effect of the code running:


Obviously I omitted the code to handle possible CommandCancelled/LispCancelled event, if the command/lisp routine to be executed could be cancelled by design, we may want to handle this event if necessary.

Followers

About Me

My photo
After graduating from university, I worked as civil engineer for more than 10 years. It was AutoCAD use that led me to the path of computer programming. Although I now do more generic business software development, such as enterprise system, timesheet, billing, web services..., AutoCAD related programming is always interesting me and I still get AutoCAD programming tasks assigned to me from time to time. So, AutoCAD goes, I go.