;;; ---------------------------------------------------------------------------
;;; TextRespace.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; RE-SPACE A COLUMN OF TEXT AT A UNIFORM PITCH
;;;
;;; PURPOSE
;;;   Takes a run of separate text lines and sets them at an even spacing,
;;;   measured from the first one and following its rotation.
;;;
;;;   The usual need is after changing the height of a block of notes: the text
;;;   is the right size but the lines are now too close or too far apart, and
;;;   moving twenty of them by hand is both slow and never quite even.
;;;
;;; HOW IT WORKS
;;;   The first item you select stays where it is and sets the direction. Every
;;;   later item is placed one further step along, perpendicular to that first
;;;   line's baseline - which is straight down for horizontal text, and correctly
;;;   sideways for text at any rotation.
;;;
;;;   Selection order is placement order, so pick them in the sequence you want
;;;   them to end up in. That also means you can reverse a block by picking it
;;;   bottom to top.
;;;
;;; THE FOUR-QUADRANT PROBLEM, SOLVED WITH ONE LINE
;;;   The routine this replaces carried a fifty-line COND with a separate branch
;;;   for text rotated 0-90, 90-180, 180-270 and 270-360 degrees, each computing
;;;   x and y with a different combination of sines and cosines.
;;;
;;;   Worked through, all four branches are the same expression. Each one is
;;;   the point reached by stepping from the first insertion point at an angle of
;;;   (rotation - 90 degrees). Taking the second branch as an example, it
;;;   computes x from cos(rot-90) and y from sin(rot-90) directly; the first
;;;   branch computes x from sin(rot) and y from -cos(rot), which are the same
;;;   two numbers by the angle-difference identities.
;;;
;;;   So the whole COND is one POLAR call, and it works at every rotation
;;;   including the ones the original's ranges left out - exactly 90, 180 and
;;;   270 degrees each fell through a boundary between two branches.
;;;
;;; JUSTIFICATION IS PRESERVED
;;;   A text object's position lives in group 10 when it is left-justified and
;;;   bottom-aligned, and in group 11 for every other justification. The original
;;;   worked that out from the FIRST item and then applied it to all of them, so
;;;   a mixed block moved the wrong point on some lines. Each item is checked
;;;   here, and both the horizontal and the vertical justification codes are
;;;   taken into account.
;;;
;;;   RESPACE  - set a run of text lines at an even spacing
;;; ---------------------------------------------------------------------------

;;; ---------------------------------------------------------------------------
;;; SESSION MEMORY
;;; ---------------------------------------------------------------------------

(if (null *TextRespace:Pitch*) (setq *TextRespace:Pitch* nil))

;;; ---------------------------------------------------------------------------
;;; WHERE A TEXT OBJECT'S POSITION LIVES
;;;
;;; Group 10 for plain left/baseline text, group 11 for anything else. Group 72
;;; is the horizontal justification and 73 the vertical; either being non-zero
;;; moves the governing point to 11.
;;;
;;; MTEXT is different again - it always uses group 10 whatever its attachment
;;; point, so it is handled explicitly rather than falling through.
;;; ---------------------------------------------------------------------------

(defun TextRespace:Code ( dat / h v )
    (cond
        ((= "MTEXT" (cdr (assoc 0 dat))) 10)
        (t
            (setq h (cdr (assoc 72 dat))
                  v (cdr (assoc 73 dat)))
            (if (or (and h (/= 0 h)) (and v (/= 0 v))) 11 10))
    )
)

;;; Rotation of a text or mtext object.
(defun TextRespace:Rotation ( dat / r )
    (setq r (cdr (assoc 50 dat)))
    (if r r 0.0)
)

;;; ---------------------------------------------------------------------------
;;; MAIN COMMAND
;;; ---------------------------------------------------------------------------

(defun c:RESPACE ( / *error* vars vals pitch ss n i ent dat code
                     firstPt rot step moved )

    (setq vars '("CMDECHO" "OSMODE")
          vals (mapcar 'getvar vars))

    (defun TextRespace: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 )
        (TextRespace:Restore)
        (if (and msg (not (wcmatch (strcase msg t) "*break*,*cancel*,*exit*")))
            (princ (strcat "\n** RESPACE 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. Restore does,
    ;; to close this undo group.
    (vl-catch-all-apply '(lambda ( ) (*push-error-using-command*)) '())
    (command "_.UNDO" "_Begin")

    ;; Spacing, remembered between runs - it is usually the same for a whole
    ;; drawing and retyping it every time is the annoying part.
    (initget 6)
    (setq pitch (getdist (strcat "\nSpacing between lines"
                                 (if *TextRespace:Pitch*
                                     (strcat " <" (rtos *TextRespace:Pitch* 2 4) ">")
                                     "")
                                 ": ")))
    (if pitch
        (setq *TextRespace:Pitch* pitch)
        (setq pitch *TextRespace:Pitch*))

    (if (null pitch)
        (princ "\nNo spacing given.")
        (progn
            (princ "\nSelect the text lines IN THE ORDER they should end up: ")
            (setq ss (ssget '((0 . "TEXT,MTEXT"))))

            (cond
                ((null ss) (princ "\nNothing selected."))

                ((< (setq n (sslength ss)) 2)
                 (princ "\nSelect at least two lines - the first one sets the position."))

                (t
                    (setvar "OSMODE" 0)
                    (setq i 0 moved 0)

                    (repeat n
                        (setq ent  (ssname ss i)
                              dat  (entget ent)
                              code (TextRespace:Code dat))

                        (if (zerop i)
                            ;; The first stays put and fixes both the origin and
                            ;; the direction everything else steps along.
                            (setq firstPt (cdr (assoc code dat))
                                  rot     (TextRespace:Rotation dat)
                                  ;; Perpendicular to the baseline: straight down
                                  ;; for horizontal text, correctly sideways for
                                  ;; anything rotated.
                                  step    (- rot (* pi 0.5)))

                            (progn
                                (setq dat (subst
                                              (cons code (polar firstPt step (* i pitch)))
                                              (assoc code dat)
                                              dat))
                                ;; A TEXT object that uses group 11 still carries
                                ;; a group 10, and leaving it behind confuses
                                ;; anything that reads the entity later.
                                (if (and (= code 11) (assoc 10 dat))
                                    (setq dat (subst
                                                  (cons 10 (polar firstPt step (* i pitch)))
                                                  (assoc 10 dat)
                                                  dat)))
                                (entmod dat)
                                (setq moved (1+ moved))))

                        (setq i (1+ i)))

                    (princ (strcat "\n" (itoa moved) " line(s) respaced at "
                                   (rtos pitch 2 4) "."))
                )
            )
        )
    )

    (TextRespace:Restore)
    (princ)
)

(princ)
