Author Topic: AutoLISP - problems with "undo" after moving objects with a reactor  (Read 1624 times)

0 Members and 1 Guest are viewing this topic.

BKolbuszewski

  • Mosquito
  • Posts: 9
This is the third post in my cycle about the table of annotative markers. First I'll give a recap of what I want my LISP to do.

I am creating a template for my team of drafters to use. We have a set of commonly used objects (les't call them "markers"), like section markers, leaders, dimensions, title blocks, etc. all set up in a table (not a real "Table", just arranged neatly in a rectangle). All of the markers are annotative, so that we do not need a separate set of markers for each os the scales that we work on.

Now we come to the problem: Although the markers, being annotative, scale properly when the annotation scale is changed, they all scale around their own basepoints (as they should). The problem is, that if these markers get too big due to the scale changes, they start to overlap so heavily, that they become totally unusable. I would like for them to move away from one another, so that they maintain their relative distances unchanged. In other words, I kinda want the whole table to behave like it is annotative. I know that it would be easy to solve by just making the whole table into an annotative block, but the markers need to be easily accessible for the drafters to use. I don't want them to have to explode the table-block each time they need to take a marker, or to have to go inside of this block, copy the markers they want, come out of the block and only then use them.

I have written the following routine that handles this task:
Code: [Select]
(setq MarkersInTable nil)
(setq oldentityMidpointList nil)

(vl-load-com)

(defun OldScaleInInches (reactorObject data / )

    (if (= (strcase (car data)) "CANNOSCALE") (progn
        (setq CANNOSCALE (getvar 'CANNOSCALE ))
        (setq oldscale (ImperialScaleTruncator CANNOSCALE "="))
        (setq oldscalefactor (Combined oldscale))
        (TableBasepoint)
        (oldscaleMarkers)
    ))
)

(defun NewScaleInInchesANDEVERYTHING (reactorObject data / )

    (if (= (strcase (car data)) "CANNOSCALE") (progn
        (setq CANNOSCALE (getvar 'CANNOSCALE ))
        (setq newscale (ImperialScaleTruncator CANNOSCALE "="))
        (setq newscalefactor (Combined newscale))
        (setq conversionfactor (/ (float oldscalefactor) (float newscalefactor)))
      (newscaleMarkerMidpoints)
       
    ))
)

(defun test (pointA pointB factor / Ax Ay Bx By)

  (setq Ax (car pointA))
  (setq Ay (cadr pointA))
  (setq Bx (car pointB))
  (setq By (cadr pointB))

  (setq Cx (+ Ax (* factor (- Bx Ax))))
  (setq Cy (+ Ay (* factor (- By Ay))))

  (setq pointC (list Cx Cy 0.0))

)

(defun newscaleMarkerMidpoints (/)

  (if MarkersInTable
      (progn
        (setq j 0)
        (while (setq ename (ssname MarkersInTable j))
          (setq entity (vlax-ename->vla-object ename))
          (setq entityLayer (vla-get-Layer entity)) ; Get the layer of the entity

              (progn
                (setq coordinates (vla-getboundingbox entity 'minPTmark 'maxPTmark))
                (setq minPTmark (vlax-safearray->list minPTmark))
                (setq maxPTmark (vlax-safearray->list maxPTmark))
                (setq entityMidpoint (list (/ (+ (car minPTmark) (car maxPTmark)) 2.0) (/ (+ (cadr minPTmark) (cadr maxPTmark)) 2.0) 0.0))
                (setq basePointList-2D (list xCoord yCoord))
                (setq newEntityMidpoint (test basePointList-2D entityMidpoint conversionfactor))

                (setq oldentityMidpoint (nth j oldentityMidpointList))

                (setq oldnewEntityMidpoint (test basePointList-2D oldentityMidpoint conversionfactor))

                (vla-move entity entityMidpoint oldentityMidpoint) ;This is a corrective move (back from the midpoint obtained after scaling to the one the entity had before it)

                (vla-move entity oldentityMidpoint oldnewEntityMidpoint) ;This is the main move

                (if (OR (= "AcDbHatch" (vla-get-objectname entity)) (= "AcDbPolyline" (vla-get-objectname entity))) (vla-ScaleEntity entity oldnewEntityMidpoint conversionfactor))
                (setq coordinates (vla-getboundingbox entity 'minPTdim 'maxPTdim))
                (setq minPTdim (vlax-safearray->list minPTdim))
                (setq maxPTdim (vlax-safearray->list maxPTdim))
                (setq dimMidpoint (list (/ (+ (car minPTdim) (car maxPTdim)) 2.0) (/ (+ (cadr minPTdim) (cadr maxPTdim)) 2.0) 0.0))

                (vla-move entity dimMidpoint oldnewEntityMidpoint) ; This is a corrective move, mainly for dimensions - for the rest of the markers it seems to do nothing (dimMidpoint = oldnewEntityMidpoit), but I kept it for all objects just in case
              )
          (setq j (1+ j))
        )
      )
      (print "\nNo objects found inside the table border.")
    )
)

(defun oldscaleMarkers (/) ; This extracts the coordinates of two opposite points of the table border and creates a rectangle based on them; and later selects all object inside this rectangle
  (setq oldentityMidpointList nil) ; Initialize the list here
  (setq tableBorder (ssget "X" (list (cons 8 "TABLE (NON-PRINTABLE) -NOVA"))))

  (if (setq ename (ssname tableBorder 0))
    (progn
      (setq tableBorder (vlax-ename->vla-object ename))

      (setq coordinates (VLA-GETBOUNDINGBOX tableBorder 'minPT 'maxPT)) ; The 'minPT and 'maxPT are output variables returned as a sefearray (I don't know why they are specified in that particular case)
      (setq minPT (vlax-safearray->list minPT)) ; This converts the minPT of a table border to a readable list
      (setq maxPT (vlax-safearray->list maxPT)) ; This converts the maxPT of a table border to a readable list

      (setq MarkersInTable (ssget "_W" minPT maxPT '((8 . "~TABLE (NON-PRINTABLE) -NOVA")))) ; THIS DEALS WITH THE EXCLUSION OF THE TABLE BORDER FROM THE SELECTION THE PROPER WAY.

      (if MarkersInTable
      (progn
        (setq i 0)
        (while (setq ename (ssname MarkersInTable i))
          (setq oldentity (vlax-ename->vla-object ename))
          (setq oldentityLayer (vla-get-Layer oldentity)) ; Get the layer of the entity

              (progn
                (setq oldcoordinates (vla-getboundingbox oldentity 'oldminPTmark 'oldmaxPTmark))
                (setq oldminPTmark (vlax-safearray->list oldminPTmark))
                (setq oldmaxPTmark (vlax-safearray->list oldmaxPTmark))
                (setq oldentityMidpoint (list (/ (+ (car oldminPTmark) (car oldmaxPTmark)) 2.0) (/ (+ (cadr oldminPTmark) (cadr oldmaxPTmark)) 2.0) 0.0))

                (setq oldentityMidpointList (cons oldentityMidpoint oldentityMidpointList))
              )
          (setq i (1+ i))
        )
        (setq oldentityMidpointList (reverse oldentityMidpointList))
      )
      (print "\nNo objects found inside the table border.")
    )
      (princ)
    )

    (print "\nNo block found on the specified layer.")
  )
)

(defun ImperialScaleTruncator (txt separator / index result)
    (setq index(vl-string-search separator txt))
      (progn
        (setq result (substr txt 1 index))
      )
    result
   )

(defun Combined (scale /)

  (cond
    ((or (and (/=(vl-string-search "/" scale) nil) (=(vl-string-search "-" scale) nil)) (and (/=(vl-string-search "/" scale) nil) (/=(vl-string-search "-" scale) nil))) (Hard scale))
    ((or (and (=(vl-string-search "/" scale) nil) (/=(vl-string-search "-" scale) nil)) (and (=(vl-string-search "/" scale) nil) (=(vl-string-search "-" scale) nil))) (Easy scale))
  )

)

(defun Easy (scaleE /)
  (cond
    ((and (=(vl-string-search "/" scaleE) nil) (/=(vl-string-search "-" scaleE) nil)) (*(atoi (chr (car (vl-string->list scaleE)))) 12))
    ((and (=(vl-string-search "/" scaleE) nil) (=(vl-string-search "-" scaleE) nil)) (atoi (chr (car (vl-string->list scaleE)))))
  )
)

(defun Hard (scaleH / x y xindex ystartindex ylength)

(setq xindex (- (vl-string-search "/" scaleH) 1))
(setq ystartindex (+ (vl-string-search "/" scaleH) 1))
(setq ylength (- (vl-string-search "\"" scaleH)  ystartindex)) ; The escape character ("\") is NOT counted when determining indexes
(setq x (atoi(substr scaleH (+ xindex 1) 1))) ; I have to add 1 to the xindex, cause FOR SOME STUPID REASON substr starts counting from "1", while everything else starts from "0"...
(setq y (atoi(substr scaleH (+ ystartindex 1) ylength))) ; I have to add 1 to the xindex, cause FOR SOME STUPID REASON substr starts counting from "1", while everything else starts from "0"...

  (cond
    ((and (/=(vl-string-search "/" scaleH) nil) (=(vl-string-search "-" scaleH) nil)) (/ (float x) (float y))) ; In division at least one number has to have a decimal expansion, in order for the result to not be an integer. That's why there's (float x) instead of just x
    ((and (/=(vl-string-search "/" scaleH) nil) (/=(vl-string-search "-" scaleH) nil)) (+ (atoi (chr (car (vl-string->list scaleH)))) (/ (float x) (float y)))) ; In division at least one number has to have a decimal expansion, in order for the result to not be an integer. That's why there's (float x) instead of just x
  )
)

(defun TableSelect(/) ; This selects the table border
(ssget "X" (list (cons 8 "TABLE (NON-PRINTABLE) -NOVA"))) ; IF THE LAYER OF THE BORDER EVER CHANGES IT HAS TO BE UPDATED HERE
)

(defun TableBasepoint (/) ; This gives me the coordinates of the table border's basepoint
  (setq tableBorder (TableSelect))
  (if (setq ename (ssname tableBorder 0))
    (progn
      (setq tableBorder (vlax-ename->vla-object ename))
      (setq basePoint (vlax-get-property tableBorder 'InsertionPoint)) ; This gives me the basepoint as a so called "safearray". So it does kinda work, but the coordinates are not readable
      (setq basePointList (vlax-safearray->list basePoint)) ; This creates a readable list out of the safearray (the list looks like this: (x y z))
      (setq xCoord (car basePointList))
      (setq yCoord (cadr basePointList))
      (setq zCoord (caddr basePointList))
    )
    (print "\nNo block found on the specified layer.")
  )
)

;Reactor
(vlr-editor-reactor nil '((:VLR-sysVarWillChange . OldScaleInInches) (:VLR-sysVarChanged . NewScaleInInchesANDEVERYTHING)))
I know it is messy, has no "error handling" and so on, but it does actually work the way I want it to (even if sometimes I do not understand the reason for some of the parts I had to include, like the seccond corrective move I have to do for dimensions, cause otherwise they end up in the wrong place).
The only thing that is bothering me here, is the fact that this routine is not fully "undoable" - the AutoCAD undo does not work on it the way it should. What ends up happening, is that the elements are moved to points around twice as close to one another as they should be (photos attached). This of course does not happen when I manually set the scale back to the original one - in that case everything works just fine.
I have found the two vla-undo functions (vla-StartUndoMark and vla-EndUndoMark), but I do not fully understand them and cannot get them to work (I did manage to crash my CAD a couple of times though). How does one use these functions with reactors? The result I am after is of course for the undo command invoked by the user to completely undo the whole LISP - so that the whole drawing is in the exact state it was before the first reactor triggered.
I am also attaching the drawing I use to test the LISP.

mhupp

  • Bull Frog
  • Posts: 250
Re: AutoLISP - problems with "undo" after moving objects with a reactor
« Reply #1 on: August 11, 2023, 12:16:21 AM »
I know that it would be easy to solve by just making the whole table into an annotative block, but the markers need to be easily accessible for the drafters to use. I don't want them to have to explode the table-block each time they need to take a marker, or to have to go inside of this block, copy the markers they want, come out of the block and only then use them.

This is where over thinking the problem is the problem.

Just make table-block then when things are starting to overlap

You can call a lisp Refresh_Marker_Table.lsp or RMT.lsp

Detect (everything inside TABLE BORDER block)
find lower left point
delete all markers inside table boarder block
re-insert the table-block at the LL point. and explode.

Using insert command with * at the front of the block path will automatically explode the block when its done inserting. allowing the user to freely select the markers again.

Code - Auto/Visual Lisp: [Select]
  1. (command "_.Insert" "*C:\blocks\table-block.dwg" LL "" "" "")


-Edit
To use the vla undo functions you have to tell autocad what "document" to use it on
Code - Auto/Visual Lisp: [Select]
« Last Edit: August 11, 2023, 01:53:34 AM by mhupp »

BKolbuszewski

  • Mosquito
  • Posts: 9
Re: AutoLISP - problems with "undo" after moving objects with a reactor
« Reply #2 on: August 11, 2023, 04:28:57 AM »
Yes, well, the problem with this approach is that when I make the whole table into an annotative block it obviously scales properly, but the other blocks inside (the markers) are exploaded (and they stay exploaded after I explode the table-block). Pictures attached. So this kinda defeats the pupouse, as I need these to be annotative blocks. I know my explanation for why I do not want to go down this road souded a bit hand-wavy in the original post, but I did actually try it first - it just turned out to generate problems I did not know how to overcome, so I settled for the "over-thought" approach.
About the vla-undo functions - Where should I put them, exactly? I did know about the
Code: [Select]
(vla-get-activedocument (vlax-get-acad-object)) part (didn't know what it does though, so thanks for the insight) and I tried to use it like that, but it didn't work. I have two main functions that are called by the reactors - one on sysVarWillChange and one on sysVarChanged events. Should the vla-StartUndoMark be at the beginning of the one in sysVarWillChange and the vla-EndUndoMark at the end of the one in sysVarChanged?

Lee Mac

  • Seagull
  • Posts: 12906
  • London, England
Re: AutoLISP - problems with "undo" after moving objects with a reactor
« Reply #3 on: August 11, 2023, 06:27:59 AM »
Yes, well, the problem with this approach is that when I make the whole table into an annotative block it obviously scales properly, but the other blocks inside (the markers) are exploaded (and they stay exploaded after I explode the table-block).

This should not be the case if the table block contains the marker block references as nested blocks, as exploding the table block will not exploded nested block references (unless the explode operation were to be called recursively).

BKolbuszewski

  • Mosquito
  • Posts: 9
Re: AutoLISP - problems with "undo" after moving objects with a reactor
« Reply #4 on: August 11, 2023, 07:25:06 AM »
Ok, so when I first created the block with just the border and then copied the markers into it, it worked - it didn't explode the blocks inside. Is this the proper way of "nesting" blocks? I can't just select everything and make a block out of it...?
But anyway, there is another problem here, that I don't think will be easily solvable (I mean that maybe my "over-thinking" will end up being similarly complex).
Because the whole table block is annotative, when I change the scale it scales accordingly (as it should). This means, that the markers that are part of it get not only "moved", but also scaled as a result. But then, because the markers are themselves annotative, they scale again (every around its own basepoint this time), so the result is, that they get "moved" once but scaled twice. Which is of course not the behavior I want - with large relative scale changes it is only mariginally better from not moving at all.
It kinda seems unavoidable with nested annotative blocks when you think about it - it is all a logical behavior.

mhupp

  • Bull Frog
  • Posts: 250
Re: AutoLISP - problems with "undo" after moving objects with a reactor
« Reply #5 on: August 11, 2023, 09:18:40 AM »
I don't think that's how Annotative blocks work. that would mean the scale of the block would be multiplicative based on the nesting level.

I'm not trying to make fun, their have been many times where I over thought things. How about you try it and see if it works.

"Experience is the lens through which logic gains depth and perspective."

Another option is to use a menu to input the markers rather then pulling them from a table.
http://www.theswamp.org/index.php?topic=55513.msg596841#msg596841

this has the added benefit of having them on a network location so if you need to update one of the markers its retroactive for everyone.
« Last Edit: August 11, 2023, 09:30:34 AM by mhupp »

BKolbuszewski

  • Mosquito
  • Posts: 9
Re: AutoLISP - problems with "undo" after moving objects with a reactor
« Reply #6 on: August 11, 2023, 09:41:57 AM »
I... did try it... I did not just came up with that by pure reasoning - I did it and then came up with an explanation of what I think was happening. So, sadly, it seems it is how the (nested) annotative blocks work. Pictures attached.
The "menu" thing is also something I considered, thank You for reminding me! I'll look into it - it actually seems like potentially the best solution.

Edit: Notice how only the annotative blocks inside the table are scaled twice - the pink rectangles or hatch doesn't behave that way. To me the simplest explanation is that the scale is in fact multiplicatively based on the nesting level. One interesting thing here is that technically hatch is also annotative, so it would seem that the pattern itself should also be scaled twice, but for some reason it is not. I have just tested it with some hatches inside the annotative block being annotative themselves and some not. And it actually does work in the opposite way than for the rest of the annotative objects... So the annotative hatch in an annotative block scales the pattern in relation to scale (so the whole pink cell always looks the same, which is the point for me), but if the hatch is not annotative and the block it is in is annotative, it maintains its relative pattern (in big enough scales it becomes invisible in the pink window, for example). I think this conflicting behavior of different objects disqualifies this solution, but I'd be more than happy to be proven wrong.

Edit 2: Well, I looked into the menu idea, but the one built into CAD doesn't seem to offer the ability to store all of the different types of objects I have in the table (like more complex multileaders for example - it just stores them all as the same generic multileader). Also, the blocks with attributes require to have each of them specified manually on every insertion. This is kind of tiresome, since it is often the case that not all of the attributes (or sometimes none at all) have to be changed from their default values. In such a case, right now it is possible to just take the marker from the table, put it in a proper place and you're done. But with the menu it would be required to manually specify the attributes each time.
So yeah, I gave it a shot but I do not think it is the right approach here afterall.
« Last Edit: August 17, 2023, 06:19:30 AM by BKolbuszewski »

BKolbuszewski

  • Mosquito
  • Posts: 9
Re: AutoLISP - problems with "undo" after moving objects with a reactor
« Reply #7 on: August 16, 2023, 11:45:14 AM »
I did some testing with the vla undo functions and my original LISP.
I've put the start one at the begining of newscaleMarkerMidpoints function and the end one at its end, cause it is this function that I'd call the "main one". It actually performs all the necessary moves of the objects (other functions do not make any changes to the displayed drawing, they just determine parameters for the movements executed in newscaleMarkerMidpoints).
The function definition looks like this now:
Code: [Select]
(defun newscaleMarkerMidpoints (/)

(vla-StartUndoMark (setq doc (vla-get-activedocument (vlax-get-acad-object))))

  (if MarkersInTable
      (progn
        (setq j 0)
        (while (setq ename (ssname MarkersInTable j))
          (setq entity (vlax-ename->vla-object ename))
          (setq entityLayer (vla-get-Layer entity)) ; Get the layer of the entity

              (progn
                (setq coordinates (vla-getboundingbox entity 'minPTmark 'maxPTmark))
                (setq minPTmark (vlax-safearray->list minPTmark))
                (setq maxPTmark (vlax-safearray->list maxPTmark))
                (setq entityMidpoint (list (/ (+ (car minPTmark) (car maxPTmark)) 2.0) (/ (+ (cadr minPTmark) (cadr maxPTmark)) 2.0) 0.0))
                (setq basePointList-2D (list xCoord yCoord))
                (setq newEntityMidpoint (test basePointList-2D entityMidpoint conversionfactor))

                (setq oldentityMidpoint (nth j oldentityMidpointList))

                (setq oldnewEntityMidpoint (test basePointList-2D oldentityMidpoint conversionfactor))

                (vla-move entity entityMidpoint oldentityMidpoint) ;This is a corrective move (back from the midpoint obtained after scaling to the one the entity had before it)

                (vla-move entity oldentityMidpoint oldnewEntityMidpoint) ;This is the main move

                (if (OR (= "AcDbHatch" (vla-get-objectname entity)) (= "AcDbPolyline" (vla-get-objectname entity))) (vla-ScaleEntity entity oldnewEntityMidpoint conversionfactor))
                (setq coordinates (vla-getboundingbox entity 'minPTdim 'maxPTdim))
                (setq minPTdim (vlax-safearray->list minPTdim))
                (setq maxPTdim (vlax-safearray->list maxPTdim))
                (setq dimMidpoint (list (/ (+ (car minPTdim) (car maxPTdim)) 2.0) (/ (+ (cadr minPTdim) (cadr maxPTdim)) 2.0) 0.0))

                (vla-move entity dimMidpoint oldnewEntityMidpoint) ; This is a corrective move, mainly for dimensions - for the rest of the markers it seems to do nothing (dimMidpoint = oldnewEntityMidpoit), but I kept it for all objects just in case
              )
          (setq j (1+ j))
        )
      )
      (print "\nNo objects found inside the table border.")
    )

(vla-EndUndoMark doc)
)
The rest of the code remains unchanged.
Now, I am obviously not writing this post because everything works smoothly... At first I though it didn't work at all, but I have noticed an interesting pattern of behavior, which I am sadly unable to decipher on my own.

1. After I run UNDO once, it basically makes the elements move to their previous positions (though not all of them - dimensions and some of the leaders are not moved back correctly), but it doesnt scale them back (understandable, since CANNOSCALE hasn't been undone yet - the scale is still the same as after the initial change).
2. After the second UNDO is run, it scales the markers properly (CANNOSCALE has been undone now - we are back to its value before the initial change, so it is logical), but it moves some of them around seemingly randomly. In the attached picture some of the markers appear to be not scaled back properly - it is not true, since if i run a REGEN they are displayed as scaled to their proper size. I could not run REGEN at that point though, because of the next step.
3. Now we come to the really interesting part... If I run a REDO after the two UNDOs, it acctually fixes everything and returns to the initial state - it is back at the point I would like a single undo to take it.
All of it is illustrated with the attached pictures.

Now, I was not able to come up with any reasonable explanation for this behavior, but it is very consistent and works even when I change the scale multiple times - if I change the scale twice and want to go back to the begining, I have to run UNDO, UNDO, REDO, UNDO, UNDO, REDO; if I change it three times I have to run the sequence thrice and so on.
Doas anyone have any idea as to what might be causing this weird behavior?
« Last Edit: August 17, 2023, 06:11:41 AM by BKolbuszewski »