Author Topic: Editor.Command() vs Editor.CommandAsync()  (Read 26476 times)

0 Members and 1 Guest are viewing this topic.

gile

  • Gator
  • Posts: 2507
  • Marseille, France
Editor.Command() vs Editor.CommandAsync()
« on: March 22, 2015, 12:21:16 PM »
Hi,

Since AutoCAD 2015, two instance methods have been added to the Editor class: Editor.Command() and Editor.CommandAsync().
These method replace the need to P/Invoke acedCmd() or call the non-public Editor.RunCommand() to synchronously run AutoCAD commands.
Note that these methods are no longer available.

The docs about these methods are more than laconic: "This is Command, a member of class Editor."
Googling for CommandAsync I only found 3 examples: a thread on Kean Walmsley's blog, another one from AutoCAD DevBlog and a code sample on GitHub.
So, I made some tests to try to deeply understand how work these new methods and share the results here.


About the methods arguments type

Both Editor.Command() and Editor.CommandAsync() accept a params array of object as argument. This means the arguments (the command name and the command inputs) can be passed separated by a comma (no need to build an array as in the examples on the Adn DevBlog and GitHub).
The command inputs can be any strong .NET type (int, double, Point2d, Point3d, ObjectId, SelectionSet, ...).
As with the LISP function command, numbers or points can be passed as string (as on command line).
An empty string ("") stands for Enter and backslash (@"\") for a pause for user input. For the last one, a constant have added to the Editor class : Editor.PauseToken.


Command() vs CommandAsync()

That said, the main question is: what is the difference between Command() and CommandAsync(), in other words, when must we have to use CommandAsync() rather than Command().
This seems to be the purpose of Kean's example, but I do not think it is very relevent: the Editor.Command() also accepts pauses for user input.

Going a little further in the comparison with de command LISP function, I tried to mimic some LISP behaviors:
- call multiple commands in a single expression:
Code - Auto/Visual Lisp: [Select]
  1. (command "_.ZOOM" "_object" (entlast) "" "_ZOOM" ".8x")
- split the the command arguments in multiple expressions:
Code - Auto/Visual Lisp: [Select]
  1. (command "_.CIRCLE")
  2. (command "0,0")
  3. (command "10")

In both cases, it requires to use CommandAsync(). With Command() method, we have to call it for each command (the following ZoomEntLast() method is also used in later examples)
Code - C#: [Select]
  1.         public void ZoomEntLast()
  2.         {
  3.             Editor ed = Application.DocumentManager.MdiActiveDocument.Editor;
  4.             ObjectId entlast = Autodesk.AutoCAD.Internal.Utils.EntLast();
  5.             if (entlast != ObjectId.Null)
  6.             {
  7.                 ed.Command("_.ZOOM", "_object", entlast, "");
  8.                 ed.Command("_.ZOOM", ".8x");
  9.             }
  10.         }
  11.  
  12.         [CommandMethod("CMD1")]
  13.         public async void Cmd1()
  14.         {
  15.             Editor ed = Application.DocumentManager.MdiActiveDocument.Editor;
  16.             await ed.CommandAsync(
  17.                 "_.LINE", new Point3d(-10.0, -10.0, 0.0), new Point3d(10.0, 10.0, 0.0), "",
  18.                 "_.CIRCLE", Point3d.Origin, 10.0);
  19.             ZoomEntLast();
  20.         }
  21.  
  22.         [CommandMethod("CMD2")]
  23.         public async void Cmd2()
  24.         {
  25.             Editor ed = Application.DocumentManager.MdiActiveDocument.Editor;
  26.             await ed.CommandAsync("_.CIRCLE");
  27.             await ed.CommandAsync("0,0");
  28.             await ed.CommandAsync("10");
  29.             ZoomEntLast();
  30.         }

The last shown behavior is interesting, for example, to pass a list of arguments to a command (e.g. a points list for LINE, PLINE, SPLINE).
This also requires the CommandAsync using.
Code - C#: [Select]
  1.         [CommandMethod("CMD3")]
  2.         public async void Cmd3()
  3.         {
  4.             Editor ed = Application.DocumentManager.MdiActiveDocument.Editor;
  5.             Point2d[] pts =
  6.             {
  7.                 new Point2d(0.0, -10.0),
  8.                 new Point2d(10.0, 0.0),
  9.                 new Point2d(0.0, 10.0),
  10.                 new Point2d(-10.0, 0.0)
  11.             };
  12.             await ed.CommandAsync("_.PLINE");
  13.             foreach (Point2d pt in pts)
  14.             {
  15.                 await ed.CommandAsync(pt);
  16.             }
  17.             await ed.CommandAsync("_close");
  18.             ZoomEntLast();
  19.         }


Synchronicity

Another "advanced using of command" in LISP is to launch a command and let the user complete it before continuing the program.
Code - Auto/Visual Lisp: [Select]
  1.   (command "_.PLINE")
  2.   (while (< 0 (getvar "CMDACTIVE"))
  3.     (command pause)
  4.   )

With the Editor.Command() method, all the 'while stuff' is implicit, and the program will wait for the command completed:
Code - C#: [Select]
  1.         [CommandMethod("CMD4")]
  2.         public void Cmd4()
  3.         {
  4.             Editor ed = Application.DocumentManager.MdiActiveDocument.Editor;
  5.             ed.Command("_.PLINE");
  6.             ZoomEntLast();
  7.         }

To be synchronous, the Editor.CommandAsync() needs to explicitly complete the command (iow do the while stuff as in LISP). This may be interesting if some other task may interact during the user inputs, for example, collecting the newly created lines:
Code - C#: [Select]
  1.         [CommandMethod("CMD5")]
  2.         public async void Cmd5()
  3.         {
  4.             Editor ed = Application.DocumentManager.MdiActiveDocument.Editor;
  5.             HashSet<ObjectId> ids = new HashSet<ObjectId>();
  6.  
  7.             await ed.CommandAsync("_.LINE", Editor.PauseToken);
  8.             while (((string)Application.GetSystemVariable("CMDNAMES")).Contains("LINE"))
  9.             {
  10.                 try
  11.                 {
  12.                     await ed.CommandAsync(Editor.PauseToken);
  13.                     ids.Add(Autodesk.AutoCAD.Internal.Utils.EntLast());
  14.                 }
  15.                 catch { break; } // eUserBreak (Cancel) handling
  16.             }
  17.  
  18.             Database db = HostApplicationServices.WorkingDatabase;
  19.             using (Transaction tr = db.TransactionManager.StartTransaction())
  20.             {
  21.                 foreach (ObjectId id in ids)
  22.                 {
  23.                     Entity ent = (Entity)tr.GetObject(id, OpenMode.ForWrite);
  24.                     ent.ColorIndex = 1;
  25.                 }
  26.                 tr.Commit();
  27.             }
  28.         }
Speaking English as a French Frog

Andrey Bushman

  • Swamp Rat
  • Posts: 864
Re: Editor.Command() vs Editor.CommandAsync()
« Reply #1 on: March 22, 2015, 02:05:39 PM »
Thank you, gile. It is useful and interesting.

Keith Brown

  • Swamp Rat
  • Posts: 601
Re: Editor.Command() vs Editor.CommandAsync()
« Reply #2 on: March 23, 2015, 12:23:54 PM »
Hi Gile,


Thank you for the information.  It was a good read.  I do not know about anyone else but I would certainly be interested in helping fund a kickstarter campaign for you to start writing a book on C# and the AutoCAD .net API!  It is something for you to seriously think about!
Keith Brown | AutoCAD MEP Blog | RSS Feed
AutoCAD MEP 2014 / Revit MEP 2014 / EastCoast CAD/CAM addon / Visual Studio 2013

MexicanCustard

  • Swamp Rat
  • Posts: 705
Re: Editor.Command() vs Editor.CommandAsync()
« Reply #3 on: March 23, 2015, 02:16:04 PM »
Hi Gile,


Thank you for the information.  It was a good read.  I do not know about anyone else but I would certainly be interested in helping fund a kickstarter campaign for you to start writing a book on C# and the AutoCAD .net API!  It is something for you to seriously think about!

Second That!
Revit 2019, AMEP 2019 64bit Win 10

Andrey Bushman

  • Swamp Rat
  • Posts: 864
Re: Editor.Command() vs Editor.CommandAsync()
« Reply #4 on: March 23, 2015, 02:20:09 PM »
I do not know about anyone else but I would certainly be interested in helping fund a kickstarter campaign for you to start writing a book on C# and the AutoCAD .net API!
What book? Is exist some content of this?

gile

  • Gator
  • Posts: 2507
  • Marseille, France
Re: Editor.Command() vs Editor.CommandAsync()
« Reply #5 on: March 23, 2015, 02:38:53 PM »
Hi Gile,


Thank you for the information.  It was a good read.  I do not know about anyone else but I would certainly be interested in helping fund a kickstarter campaign for you to start writing a book on C# and the AutoCAD .net API!  It is something for you to seriously think about!

Second That!

Thank you very much both, this is very kind of you, but writing a book about AutoCAD .NET API is really very high over my head (even in French, my native language).
Anyway, I'm glad to share what I discover in the API regions I explore.
Speaking English as a French Frog

Keith Brown

  • Swamp Rat
  • Posts: 601
Re: Editor.Command() vs Editor.CommandAsync()
« Reply #6 on: March 23, 2015, 03:18:06 PM »
What book? Is exist some content of this?


No book yet Andrey.  I am trying to convince gile to write one.  I would happily help fund it through a crowd sourcing campaign such as Kickstarter or something similar.


Gile,  I believe that you put too much emphasis on your "supposed" language handicap.  You explain things in english much better than I ever could and I have been speaking it my entire life.
Keith Brown | AutoCAD MEP Blog | RSS Feed
AutoCAD MEP 2014 / Revit MEP 2014 / EastCoast CAD/CAM addon / Visual Studio 2013

Andrey Bushman

  • Swamp Rat
  • Posts: 864
Re: Editor.Command() vs Editor.CommandAsync()
« Reply #7 on: March 23, 2015, 03:31:58 PM »
I can't transfer money, but I can share my some materials for the interesting book, if good author will be appearing.

BlackBox

  • King Gator
  • Posts: 3770
Re: Editor.Command() vs Editor.CommandAsync()
« Reply #8 on: March 23, 2015, 07:48:09 PM »
What book? Is exist some content of this?


No book yet Andrey.  I am trying to convince gile to write one.  I would happily help fund it through a crowd sourcing campaign such as Kickstarter or something similar.


Gile,  I believe that you put too much emphasis on your "supposed" language handicap.  You explain things in english much better than I ever could and I have been speaking it my entire life.

1+

This would be worth donating toward - and well done here, Gile.



FWIW - I requested Autodesk consider compensating Gile for his work on CTR custom OSNAP (.NET posted publicly here), and his OSNAPS Palette App currently published at Exchange Apps Store (as a result of the new 'independently developed' GCE feature (geometric center, aka "Gilles Chanteau Emulator") .

Attempted something similar in previous beta for Lee, for his LISP work on MText; not sure that either were successful offline, but I tried. *shrug*
"How we think determines what we do, and what we do determines what we get."

Kean

  • Newt
  • Posts: 48
Re: Editor.Command() vs Editor.CommandAsync()
« Reply #9 on: March 24, 2015, 05:15:33 AM »
Great analysis, Gile!

I'd just add a quick note... for me the intention behind these two methods is that Command() be used to execute complete commands synchronously (with the well-taken caveat that any user prompting required to complete the command will get performed) while CommandAsync() allows the application code to participate further in the command's execution (including specifying where the user should provide input).

Kean

gile

  • Gator
  • Posts: 2507
  • Marseille, France
Re: Editor.Command() vs Editor.CommandAsync()
« Reply #10 on: March 24, 2015, 05:39:12 AM »
Thanks Kean.

So, just a question: to let the user complete command inputs, is it safe to just call Command() as below (which seems to work fine) ?

Code - C#: [Select]
  1.             [CommandMethod("CMD4")]
  2.            public void Cmd4()
  3.            {
  4.                Editor ed = Application.DocumentManager.MdiActiveDocument.Editor;
  5.                ed.Command("_.PLINE");
  6.                ZoomEntLast();
  7.            }

or is it preferable to call CommandAsync() ?

Code - C#: [Select]
  1.         [CommandMethod("CMD6")]
  2.         public async void Cmd6()
  3.         {
  4.             Editor ed = Application.DocumentManager.MdiActiveDocument.Editor;
  5.             await ed.CommandAsync("_.PLINE");
  6.             while (((string)Application.GetSystemVariable("CMDNAMES")).Contains("PLINE"))
  7.             {
  8.                 try { await ed.CommandAsync(Editor.PauseToken); }
  9.                 catch { break; }
  10.             }
  11.             ZoomEntLast();
  12.         }
Speaking English as a French Frog

Kean

  • Newt
  • Posts: 48
Re: Editor.Command() vs Editor.CommandAsync()
« Reply #11 on: March 24, 2015, 06:22:09 AM »
So, just a question: to let the user complete command inputs, is it safe to just call Command() as below (which seems to work fine) ?

Good question...

Editor.Command() calls through to acedCmdS(). It's a slightly different code path from acedCommandS(), for which the ObjectARX docs say:

"However, if acedCommandS is not suitable if you want to pass in enough tokens to begin a command but not finish it. When the tokens run out, any command in progress is cancelled. Partially completed command side effects are not rolled back."

Clearly that's not the behaviour we're seeing here, but my personal instinct would be to avoid relying on acedCmdS() completing partial commands (rather than cancelling them, as documented for acedCommandS()).

Kean

owenwengerd

  • Bull Frog
  • Posts: 451
Re: Editor.Command() vs Editor.CommandAsync()
« Reply #12 on: March 24, 2015, 06:23:01 PM »
I believe the ARX documentation was correct for the early builds of AutoCAD 2015 (or maybe 2014?), but the implementation was subsequently changed (presumably because it was discovered that cancelling the command served no purpose except to break working code).

Kean

  • Newt
  • Posts: 48
Re: Editor.Command() vs Editor.CommandAsync()
« Reply #13 on: March 25, 2015, 02:04:49 AM »
Thanks, Owen.

I'll ask around and see what I find out.

Kean

Jeff H

  • Needs a day job
  • Posts: 6150
Re: Editor.Command() vs Editor.CommandAsync()
« Reply #14 on: March 25, 2015, 11:48:34 PM »
This is what I got out of it.

For starters I think the use Async, await might confuse or give some wrong impression like Dispose and Using statement with AutoCAD API.
Not that they misuse either but calling Dispose is usually in association with releasing unmanaged resources from memory. The API heavily deals with manipulating a AutoCAD database and the dispose calls are used to close database resident objects and you never actually delete or release memory of a database resident object. Only objects created that do not get added to a database where it then is handled by AutoCAD.
Maybe if explained in more detail how dispose is really calling close or cancel on database resident objects might help some at first.

CommandAsync calls acedcmdC
Command calls acedCmdS

Code - C#: [Select]
  1.   public unsafe void Command(params object[] parameter)
  2. {
  3.     resbuf* resbufPtr = 0L;
  4.     try
  5.     {
  6.         resbufPtr = (resbuf*) Marshaler.ObjectsToResbuf(parameter).ToPointer();
  7.         Interop.CheckAds(acedCmdS((resbuf modopt(IsConst)*) resbufPtr, false, 0L));
  8.     }
  9.     finally
  10.     {
  11.         if (resbufPtr != null)
  12.         {
  13.             acutRelRb(resbufPtr);
  14.         }
  15.     }
  16. }
  17.  
  18.  

Code - C#: [Select]
  1.  
  2.   public unsafe CommandResult CommandAsync(params object[] parameter)
  3. {
  4.     if (acedFiberWorld())
  5.     {
  6.         Interop.ThrowExceptionForErrorStatus(2);
  7.     }
  8.     resbuf* resbufPtr = 0L;
  9.     try
  10.     {
  11.         resbufPtr = (resbuf*) Marshaler.ObjectsToResbuf(parameter).ToPointer();
  12.         CommandResult result = new CommandResult();
  13.         IntPtr ptr = (IntPtr) GCHandle.Alloc(result);
  14.         Interop.CheckAds(acedCmdC((int modopt(CallConvCdecl) *(void*)) __unep@?acedCmdCallback@@$$FYAHPEAX@Z, ptr.ToPointer(), (resbuf modopt(IsConst)*) resbufPtr));
  15.         acedCallBackOnCancel();
  16.         return result;
  17.     }
  18.     finally
  19.     {
  20.         if (resbufPtr != null)
  21.         {
  22.             acutRelRb(resbufPtr);
  23.         }
  24.     }
  25.     return null;
  26. }
  27.  
  28.  
  29.  
  30.  


So a quick overview of possible behavior


acedCmdC vs acedCmdS
The "C" in acedCmdC stands for Coroutine and "S" in acedCmdS stands for Subroutine.
Very quick overview
Subroutine is invoked at beginning and runs until end or is exited from return, etc....
Coroutine is invoked and then exit calling another Coroutine, but save its state and return back to point and have multiple entry points.

Coroutines(language level) and  fibers(system level)
Coroutines are a construct at the language level. The same concept of control flow at the system level is fibers.

Fibers are depreciated in2015

AutoCAD used fibers for  document switching and command execution by swapping native code call stacks within a thread
Without fibers AutoCAD has only one code stack in its main thread.

So now to transfer control by pushing more code on the stack (subroutine style), or by returning off the call stack after queueing a message to the logic that will then be executed  (coroutine style).   

AutoCAD still operates under the concepts of “application context” and “document context”.   Except now, to reach “application context”, AutoCAD must return out of functions which are logically bound to documents, rather than perform a stack switch.

I believe Lisp processor handles itself by saving call stack, processes it, re-invokes and restores, etc... to work as it did before.

Some of the results I believe are from
If you look at docs at acedCommand Migration
Quote
// acedCommandS:  syntax-compatible replacement of acedCommand,
//                use for "full commands", not partial ones.
//                This command essentially begins with its own
//                private command line processor, initialized at
//                the "Command:" prompt.   The commands are executed
//                in the same manner as with acedCommand, except that
//                when the supplied input has all been executed,
//                the private command line processor is deleted
//                and the previous command line processor is unchanged
//                from when the call is made.   modality and
//                transparent "builtin" commands may not be nested.


// acedCmdS
//
// Does the same thing with the same restructions as acedCommandS,
// with a resbuf chain rather than a veriable arguments list.
// Two more supplied parameters are intended for future use.



// acedCommandC, acedCmdC:
// “coroutine” relationship with calling command processor.
// Queues up the resbuf chain for feeding to the
// calling command processor and stores the “returning”
// callback function pointer and data pointer.
// In the text here, they will be collectively
// referred to as "acedCmdC", which in fact is
// the common implementation.  "acedCmd" will represent
// the corresponding legacy command functions.
//
// After making call, caller must return from its
// command (or defun/regfunc) entry
// point to allow the command processor to process the
// pending command input.  Once done with that input,
// AutoCAD will call pReturnFcn, with pReturnFcnParams
// passed in.
//
// There are some common rules for the use of both acedCmdC
// and acedCmd within a recognized application callback
// (specifically, these functions are only operative within
// specific AutoCAD contexts:
// - a command, registered in ARX via AcEdCommandStack::addCommand
// - a LISP invocation of a function registered via acedDefun
//   (and/or acedRegfunc).
// - a coroutine callback passed in from acedCmdC
// - Evaluation of LISP (command) statement evaluation.
// - builtin commands (plus AcEdCommand commands with the
//   ACRX_CMD_NONEWSTACK flag set).
// - Session Fiber Commands (or Application Execution Context)
// - Transparent commands, executed at an input prompt of another
//   command in progress (Regardless of builtin or ARX-registered)
//
// 1. acedCmdC may be invoked from non-LISP calling contexts
//    regardless of the $FIBERWORLD
//    setting, (the exclusion from transparent commands remains).
//
//    From LISP contexts where acedInvoke() is active (i.e. acedDefun'ed
//    functions being called from LISP),
//    acedCmdC may only be invoked when $FIBERWORLD is 0.
//
//    acedCmd, in comparison, cannot be invoked unless $FIBERWORLD
//    is 1, if done, it will return an error status. It also
//    cannot be invoked from builtin commands, nor transparent commands.
//
//    Therefore, function logic running in an acedDefun/acedInvoke
//    context should test acedFiberWorld to determine whether
//    to run acedCmd or acedCmdC is permissible. Notice that
//    acedCmdS is safe to run in this context regardless of the
//    $FIBERWORLD setting.
//
// 2. While it is not a destination in one's migration,
//    Command and defun'ed function logic (but not coroutine callbacks,
//    described next) may invoke acedCmd any number of times,
//    but after making a call to acedCmdC, the function must
//    return without making a call to either acedCmdC nor acedCmd.
//    If such a call is made, the resbuf chain previously supplied
//    by acedCmdC will be replaced with the subsequent chain.
//    acedCmd will return an error status and take no action.
// 3. Coroutine continuation functions may call acedCmdC,
//    but use of acedCmd will return an error status
//    and no action.
// 2. acedCmdS may be used in any of these contexts (and many more)
//    and at any time within these contexts.
//    It is unobvious to invoke acedCmdS
//    after calling acedCmdC and before returning to have its
//    input procssed, since the acedCmdS will be executed
//    synchronously, but it would work.
// 3. Repeated calls to acedCmdC without returning for processing
//    in between will simply erase the earlier acedCmdC contents.
//    In other words, only one call will count before returning,
//    so don't bother making multiple calls.
// 4. Commands and LISP expressions which use different mixes of
//    acedCmd, acedCmdC and acedCmdS may invoke each other
//    without regard to the makeup of the other command/expression.
//    Command processor modality restrictions will still apply
//    such as no nesting of nontransparent commands nor transparent
//    commands.
//
// WARNING: support of these functions in logic operated through
//          invocation of functions registered via acedDefun
//          is incomplete in ACAD 2011.
//



Jeff H

  • Needs a day job
  • Posts: 6150
Re: Editor.Command() vs Editor.CommandAsync()
« Reply #15 on: March 25, 2015, 11:56:08 PM »
What I meant in previous post is you can Await anything like custom class CommandResult


Code - C#: [Select]
  1. public sealed class CommandResult : INotifyCompletion
  2. {
  3.     // Fields
  4.     private bool m_completed = false;
  5.     private Action m_continuation;
  6.  
  7.     // Methods
  8.     internal CommandResult()
  9.     {
  10.     }
  11.  
  12.     internal int Callback()
  13.     {
  14.         int num;
  15.         try
  16.         {
  17.             this.m_completed = true;
  18.             this.m_continuation();
  19.             return 0x13ec;
  20.         }
  21.         catch (Exception exception1)
  22.         {
  23.             UnhandledExceptionFilter.CerOrShowExceptionDialog(exception1);
  24.             num = -5002;
  25.         }
  26.         return num;
  27.     }
  28.  
  29.     public Editor.CommandResult GetAwaiter()
  30.     {
  31.         return this;
  32.     }
  33.  
  34.     public void GetResult()
  35.     {
  36.         if (acedCmdCWasCancelled())
  37.         {
  38.             Interop.ThrowExceptionForErrorStatus(180);
  39.         }
  40.     }
  41.  
  42.     public void OnCompleted(Action continuation)
  43.     {
  44.         this.m_continuation = continuation;
  45.     }
  46.  
  47.     // Properties
  48.     public bool IsCompleted
  49.     {
  50.         [return: MarshalAs(UnmanagedType.U1)]
  51.         get
  52.         {
  53.             return this.m_completed;
  54.         }
  55.     }
  56. }
  57.  


 Pretty neat can do it with extension methods also

http://blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115642.aspx
Code - C#: [Select]
  1. await 3000;
  2. ..
  3. ....
  4. ...
  5.  
  6.      public static class Awaiters
  7.     {
  8.         public static TaskAwaiter GetAwaiter(this TimeSpan timeSpan)
  9.         {
  10.             return Task.Delay(timeSpan).GetAwaiter();
  11.         }
  12.         public static TaskAwaiter GetAwaiter(this Int32 millisecondsDue)
  13.         {
  14.             return TimeSpan.FromMilliseconds(millisecondsDue).GetAwaiter();
  15.         }
  16.     }
  17.  


Kean

  • Newt
  • Posts: 48
Re: Editor.Command() vs Editor.CommandAsync()
« Reply #16 on: March 26, 2015, 12:37:06 PM »
Just to follow up on this... the docs do indeed need to be fixed. You can rely on Editor.Command() completing commands for you rather than cancelling them: so you should only need a while loop if you need finer-grained control over the command tokens.

Kean

Jeff H

  • Needs a day job
  • Posts: 6150
Re: Editor.Command() vs Editor.CommandAsync()
« Reply #17 on: December 11, 2023, 05:05:22 PM »
This might be an example of using CommandAsync to interact more in command. If you are wanting to use PLINE command with arcs but keep a constant angle with ability to negate/filp arc easily.
Here is another topic explaining same issue https://forums.autodesk.com/t5/visual-lisp-autolisp-and-general/draw-polyline-arc-circuits-it-essential-for-electrical-and/td-p/9837927
The weird part is to change a angle once entered the PLINE command requires using escape during the command and not letting it run through and cancel the command but now as I post it I think I could of handled this without using rightclick to end command and could of done it a little differently.

But basically
The pretranslate handlers
  • Catch spacebar and change to esc and set flag to negate angle
  • Catch right click and sends cancel and sets flag to end command
The command
  • Prompts for point/or can set angle here
  • Adds pretranslate handlers to watch for spacebar/rightclick. Spacbar filps/negates angle and rightclick ends command
  • Start the command await Ed.CommandAsync("_.PLINE", ppr.Value, "A");
  • Start a loop and check if angle needs to be negated(always false first time) and await Ed.CommandAsync("A", angle, Editor.PauseToken);
    • If point selected then loop starts back over entering previous angle
    • If spacebar pressed then catch block catches it and loop starts back over, but negating the angle, and can keep negating back and forth with spacebar
    • If rightclicked pressed then throws to outer catch block which supresses error if a user break and in its finally block removes handler
Code - C#: [Select]
  1.         private const int VK_SPACE = 32;
  2.         private const int VK_ESCAPE = 27;
  3.         const int WM_CHAR = 258;
  4.         /// </summary>
  5.         private const int MK_SHIFT = 4;
  6.         /// <summary>
  7.         /// The w m_ rbuttonup
  8.         /// </summary>
  9.         private const int WM_RBUTTONUP = 517;
  10.         /// <summary>
  11.         /// The w m_ lbuttonup
  12.         /// </summary>
  13.         private const int WM_LBUTTONUP = 514;
  14.         /// <summary>
  15.         /// The w m_ lbuttondown
  16.         /// </summary>
  17.         private const int WM_LBUTTONDOWN = 513;
  18.         /// <summary>
  19.         /// The w m_ rbuttondown
  20.         /// </summary>
  21.         private const int WM_RBUTTONDOWN = 516;
  22.  
  23.         /// <summary>
  24.         /// Handles the PreTranslateMessage event of the Application control.
  25.         /// </summary>
  26.         /// <param name="sender">The source of the event.</param>
  27.         /// <param name="e">The <see cref="PreTranslateMessageEventArgs"/> instance containing the event data.</param>
  28.         private void Application_PreTranslateMessage(object sender, PreTranslateMessageEventArgs e)
  29.         {
  30.             var wp = e.Message.wParam.ToInt64();
  31.  
  32.      
  33.             if (e.Message.message == WM_CHAR && wp == VK_SPACE)
  34.             {
  35.                 System.Windows.Interop.MSG msg = e.Message;
  36.                 negate = true;
  37.                 msg.wParam = new IntPtr(VK_ESCAPE);
  38.                 e.Message = msg;;
  39.             }
  40.  
  41.             if (e.Message.message == WM_RBUTTONDOWN && (wp != 6 && wp != MK_SHIFT))
  42.             {
  43.                 e.Handled = true;
  44.             }
  45.             if (e.Message.message == WM_RBUTTONUP && wp != MK_SHIFT)
  46.             {
  47.                 Application.PreTranslateMessage -= Application_PreTranslateMessage;
  48.                 e.Handled = true;
  49.                 endCommand = true;
  50.                 Application.DocumentManager.MdiActiveDocument.SendCancel();
  51.             }
  52.         }
  53.  
  54.         int angle = 60;
  55.         bool negate = false;
  56.         private bool endCommand = false;
  57.  
  58.         [CommandMethod("parc")]
  59.         public async void parc()
  60.         {
  61.  
  62.             negate = false;
  63.             endCommand = false;
  64.             PromptPointOptions ppo = new PromptPointOptions("\nSelect Point");
  65.             ppo.Keywords.Add("Angle");
  66.             PromptPointResult ppr = Ed.GetPoint(ppo);
  67.  
  68.             if (ppr.Status == PromptStatus.Keyword)
  69.             {
  70.                 PromptIntegerOptions pio = new PromptIntegerOptions("\nEnter Angle");
  71.                 pio.AllowNegative = true;
  72.                 pio.DefaultValue = angle;
  73.                 PromptIntegerResult pir = Ed.GetInteger(pio);
  74.                 if (pir.Status != PromptStatus.OK) return;
  75.                 angle = pir.Value;
  76.                 ppo.Keywords.Clear();
  77.                 ppr = Ed.GetPoint(ppo);
  78.             }
  79.             if (ppr.Status != PromptStatus.OK) return;
  80.  
  81.  
  82.             try
  83.             {
  84.                 Application.PreTranslateMessage += Application_PreTranslateMessage;
  85.  
  86.                 await Ed.CommandAsync("_.PLINE", ppr.Value, "A");
  87.                 while (Settings.Variables.CMDNAMES.ToUpper().IndexOf("PLINE", System.StringComparison.Ordinal) >= 0)
  88.                 {
  89.                     try
  90.                     {
  91.                         if (negate)
  92.                         {
  93.                             negate = false;
  94.                             angle = angle * -1;
  95.                         }
  96.                         await Ed.CommandAsync("A", angle, Editor.PauseToken);
  97.                     }
  98.  
  99.                     catch (Autodesk.AutoCAD.Runtime.Exception ex)
  100.                     {
  101.                         if (ex.ErrorStatus != ErrorStatus.UserBreak)
  102.                         {
  103.                             throw;
  104.                         }
  105.                         if (endCommand)
  106.                         {
  107.                             throw;
  108.                         }
  109.  
  110.                     }
  111.  
  112.                 }
  113.             }
  114.             catch (Autodesk.AutoCAD.Runtime.Exception ex)
  115.             {
  116.                 if (ex.ErrorStatus != ErrorStatus.UserBreak)
  117.                 {
  118.                     throw;
  119.                 }
  120.  
  121.             }
  122.             finally
  123.             {
  124.                 Application.PreTranslateMessage -= Application_PreTranslateMessage;
  125.  
  126.             }
  127.         }
  128.  

*EDIT*
Sorry the command had custom OSNAPS, temporary systemvariables, and DocData class used for setting and cloning layers that are mapped to commands and block names, and I thought I stripped most of it out but missed the SendCancel, and look at ealier example by gile on looping checking CMDNAMES variables
Code - C#: [Select]
  1.  
  2.         public static void SendCommand(this Document doc, string command)
  3.         {
  4.             dynamic acadDoc = doc.GetAcadDocument();
  5.             acadDoc.SendCommand(command);
  6.         }
  7.         public static void SendCancel(this Document doc)
  8.         {
  9.             doc.SendCommand("\x03\x03");
  10.         }
  11.  





« Last Edit: December 12, 2023, 01:48:48 AM by Jeff H »