;;; ---------------------------------------------------------------------------
;;; MatchRotate.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; ROTATE TO MATCH SOMETHING ELSE
;;;
;;; PURPOSE
;;;   Turns a selection so that one line in it ends up parallel to another line
;;;   you point at. No angles to work out and none to type.
;;;
;;;   For squaring a detail up to a wall that is not on a round number, lining a
;;;   note up with a pipe run, or setting a north point to a site boundary.
;;;
;;;   ALIGN does something related but not the same - it moves and scales as
;;;   well, and needs matched pairs of points. This only turns, about a base
;;;   point you choose.
;;;
;;; HOW IT WORKS
;;;   Two lines are picked. The first is the reference IN the selection - the
;;;   edge that is to end up parallel to something. The second is the target.
;;;   The difference between their angles is how far everything turns.
;;;
;;;   Both are read with NENTSEL rather than ENTSEL, so an edge inside a block
;;;   can be used without exploding it.
;;;
;;; WHAT WAS FIXED
;;;   - The saved error handler went into a global called *e*. Any other routine
;;;     using the same well-worn name - and it is a common one - would overwrite
;;;     it, and the wrong handler would be restored.
;;;   - The error handler restored APERTURE from a variable that was local to a
;;;     DIFFERENT function, so it was always nil when the handler ran and the
;;;     aperture was never actually put back. Anything interrupted mid-pick left
;;;     the pick aperture set to the pickbox size.
;;;   - It started the ROTATE command, then went off to ask three questions, and
;;;     fed the answers in afterwards - so an error in between left ROTATE
;;;     hanging at a prompt.
;;;   - ORTHOMODE was turned on and never turned off.
;;;
;;;   MATCHROT  - rotate a selection to match another line
;;; ---------------------------------------------------------------------------

;;; The angle of a line-like object, picked anywhere along it. Returns nil if
;;; what was picked has no direction.
(defun Match:AngleOf ( prompt / sel ent data kind ang )
    (setq ang nil)
    (while (and (null ang) (setq sel (nentsel prompt)))
        (setq ent  (car sel)
              data (entget ent)
              kind (cdr (assoc 0 data)))
        (cond
            ((= kind "LINE")
             (setq ang (angle (cdr (assoc 10 data)) (cdr (assoc 11 data)))))
            ((member kind '("LWPOLYLINE" "POLYLINE" "ARC" "SPLINE" "ELLIPSE"))
             ;; Use the direction of the curve where it was picked, which is
             ;; what "parallel to that" means on anything but a straight.
             (setq ang (angle '(0 0 0)
                        (vlax-curve-getFirstDeriv
                            (vlax-ename->vla-object ent)
                            (vlax-curve-getParamAtPoint
                                (vlax-ename->vla-object ent)
                                (vlax-curve-getClosestPointTo
                                    (vlax-ename->vla-object ent)
                                    (cadr sel)))))))
            (t (princ (strcat "\n  A " kind " has no direction to match.")))))
    ang
)

(defun c:MATCHROT ( / *error* vars vals ss ref target turn base v deg )

    (vl-load-com)
    (setq vars '("CMDECHO" "ORTHOMODE" "APERTURE" "OSMODE")
          vals (mapcar 'getvar vars))

    (defun Match:Restore ( )
        (mapcar 'setvar vars vals)
        (while (= 8 (logand 8 (getvar 'undoctl))) (command "_.UNDO" "_End"))
        (vl-catch-all-apply '(lambda ( ) (*pop-error-mode*)) '())
        (princ)
    )

    (defun *error* ( msg )
        (Match:Restore)
        (if (and msg (not (wcmatch (strcase msg t) "*break*,*cancel*,*exit*")))
            (princ (strcat "\n** MATCHROT error: " msg " **")))
        (princ)
    )

    (setvar "CMDECHO" 0)
    ;; AutoCAD 2015 and later refuse (command) inside an *error* handler unless
    ;; the routine says up front that it will use one.
    (vl-catch-all-apply '(lambda ( ) (*push-error-using-command*)) '())
    (command "_.UNDO" "_Begin")

    (princ "\nSelect what is to be rotated.")
    (setq ss (ssget))

    (if (null ss)
        (princ "\nNothing selected.")
        (progn
            ;; Everything is asked for BEFORE the command starts, so an error
            ;; part way cannot leave ROTATE sitting at a prompt.
            (setq ref (Match:AngleOf "\nEdge in the selection to line up: "))

            (if (null ref)
                (princ "\nCancelled.")
                (progn
                    (initget "Angle")
                    (setq target (Match:AngleOf
                        "\nLine to match it to, or type A for an angle: "))

                    (if (null target)
                        (progn
                            (initget 1)
                            (setq v (getangle "\nNew angle: "))
                            (setq target v)))

                    (if (null target)
                        (princ "\nCancelled.")
                        (progn
                            (setq base (getpoint "\nPoint to rotate about: "))

                            (if (null base)
                                (princ "\nCancelled.")
                                (progn
                                    (setq turn (- target ref)
                                          deg  (/ (* 180.0 turn) pi))
                                    ;; Report the shorter of the two equivalent
                                    ;; turns, which is what anyone checking the
                                    ;; result will measure.
                                    (while (> deg 180.0)  (setq deg (- deg 360.0)))
                                    (while (< deg -180.0) (setq deg (+ deg 360.0)))

                                    (setvar "OSMODE" 0)
                                    (command "_.ROTATE" ss "" base
                                             (/ (* 180.0 turn) pi))

                                    (princ (strcat "\nRotated " (rtos deg 2 4)
                                                   " degrees about the base point.")))))))))
    )

    (Match:Restore)
    (princ)
)

(princ)
