;;; ---------------------------------------------------------------------------
;;; DimNote.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; STANDARD NOTES ON DIMENSIONS, AND FLIPPING THEM
;;;
;;; PURPOSE
;;;   DIMNOTE adds one of the stock abbreviations - TYP., CLR., V.I.F. and the
;;;   rest - to a dimension, either alongside the figure or on a second line
;;;   under it. Typing them by hand is how you end up with TYP, TYP. and Typ.
;;;   in the same drawing.
;;;
;;;   DIMFLIP turns a dimension round, so it measures from the other end. Useful
;;;   when the arrows and text end up on the wrong side of a tight dimension.
;;;
;;; HOW THE TEXT IS BUILT
;;;   A dimension's text override is DXF group 1. Leaving it empty means "use
;;;   the measured value"; putting <> in it means the same thing but lets you
;;;   add text around it. So TYP. beside the figure is stored as "<> TYP." and
;;;   the measurement still updates when the geometry moves.
;;;
;;;   Two control sequences matter:
;;;       \X   splits the text - everything after it goes BELOW the line
;;;       \P   an ordinary line break
;;;
;;;   So the first note placed below uses \X, and any note after that uses \P,
;;;   because a second \X would be ignored. That is the one piece of cleverness
;;;   in here and it was in the original, correctly.
;;;
;;; WHAT WAS FIXED
;;;   - The dialog was in a separate dimnote.dcl that had to be found on the
;;;     support path. It is embedded here and written to a temporary file only
;;;     while it is on screen, so there is nothing to install.
;;;   - Pressing Enter instead of picking a dimension returned nil, which was
;;;     then passed to (entget nil) - an error inside a loop with no way out.
;;;   - (if (not (new_dialog ...)) (exit)) left the dialog file loaded and the
;;;     handle leaked, every time the dialog failed to open.
;;;   - The empty-note test compared against a single space rather than an empty
;;;     string, so pressing OK with the field cleared placed an empty note.
;;;   - dn_modifier was left global.
;;;   - DIMFLIP looped forever if you pressed Enter rather than selecting, and
;;;     called (command ".undo" "m") without the underscore prefix, which fails
;;;     on a non-English AutoCAD. It also flipped angular and ordinate
;;;     dimensions, where groups 13 and 14 do not mean what they mean on a
;;;     linear one, quietly producing nonsense.
;;;   - Neither had an error handler or an undo group.
;;;
;;;   DIMNOTE  - add a standard note to a dimension
;;;   DIMFLIP  - reverse a linear dimension's direction
;;; ---------------------------------------------------------------------------

;;; ---------------------------------------------------------------------------
;;; THE NOTE LIST
;;;
;;; Edit this to match your office standard. It is read once when the file
;;; loads, so change it and reload.
;;; ---------------------------------------------------------------------------

(setq DimNote:Notes
    '("TYP."      "TYPICAL"          "CLR."     "CLEAR"
      "MIN."      "MAX."             "M.O."     "R.O."
      "REF."      "V.I.F."           "VERIFY IN FIELD"
      "APPROX."   "EQ."              "O.C."     "NOM."
      "+/-"       "HOLD"             "CRITICAL"))

;;; ---------------------------------------------------------------------------
;;; THE DIALOG
;;;
;;; Held as text and written to a temporary .dcl when needed, so this file
;;; travels on its own.
;;; ---------------------------------------------------------------------------

(defun DimNote:WriteDcl ( / path fh )
    (setq path (vl-filename-mktemp "dimnote" nil ".dcl"))
    (if (setq fh (open path "w"))
        (progn
            (foreach line
               '("dimnote : dialog { label = \"Note on Dimension\";"
                 "  : row {"
                 "    : boxed_column { label = \"Standard notes\";"
                 "      : list_box { key = \"lb_notes\"; width = 26; height = 12;"
                 "                   allow_accept = true; }"
                 "    }"
                 "    : column {"
                 "      : boxed_column { label = \"Note\";"
                 "        : edit_box { key = \"eb_note\"; edit_width = 22; }"
                 "      }"
                 "      : boxed_radio_column { label = \"Placement\";"
                 "        : radio_button { key = \"dn_beside\";"
                 "                         label = \"Beside the figure\"; }"
                 "        : radio_button { key = \"dn_below\";"
                 "                         label = \"Below the line\"; }"
                 "      }"
                 "      : paragraph {"
                 "        : text_part { label = \"The measurement stays live -\"; }"
                 "        : text_part { label = \"the note is added around it.\"; }"
                 "      }"
                 "    }"
                 "  }"
                 "  : errtile { key = \"error\"; }"
                 "  ok_cancel;"
                 "}")
                (write-line line fh))
            (close fh)
            path)
        (progn
            (princ "\n** Cannot write the temporary dialog file. **")
            nil))
)

;;; ---------------------------------------------------------------------------
;;; SELECTION
;;;
;;; Ask until a dimension is picked or the user gives up. Returning nil for
;;; "gave up" is what the original lacked - it had no way to stop asking.
;;; ---------------------------------------------------------------------------

(defun DimNote:PickDim ( prompt linearOnly / sel ent data got kind )
    (setq got nil)
    (while (and (null got)
                (setq sel (entsel (strcat "\n" prompt " <Enter to give up>: "))))
        (setq ent  (car sel)
              data (entget ent))
        (cond
            ((/= "DIMENSION" (cdr (assoc 0 data)))
             (princ "\n  That is not a dimension."))

            ;; Group 70 carries the dimension type in its low three bits:
            ;; 0 rotated/linear, 1 aligned, 2 angular, 3 diameter, 4 radius,
            ;; 5 angular 3-point, 6 ordinate.
            ((and linearOnly
                  (not (member (setq kind (logand 7 (cdr (assoc 70 data))))
                               '(0 1))))
             (princ (strcat "\n  That is "
                            (cond ((= kind 2) "an angular")
                                  ((= kind 3) "a diameter")
                                  ((= kind 4) "a radius")
                                  ((= kind 5) "an angular")
                                  ((= kind 6) "an ordinate")
                                  (t "an unusual"))
                            " dimension - only linear and aligned ones can be"
                            " flipped this way.")))

            (t (setq got ent))))
    got
)

;;; ---------------------------------------------------------------------------
;;; DIMNOTE
;;; ---------------------------------------------------------------------------

(defun c:DIMNOTE ( / *error* vars vals dcl id note below ent data old new
                     result path )

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

    (defun DimNote:Restore ( )
        (if id (vl-catch-all-apply 'unload_dialog (list id)))
        (setq id nil)
        (if (and path (findfile path))
            (vl-catch-all-apply 'vl-file-delete (list path)))
        (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 )
        (DimNote:Restore)
        (if (and msg (not (wcmatch (strcase msg t) "*break*,*cancel*,*exit*")))
            (princ (strcat "\n** DIMNOTE 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")

    ;; BELOW starts at 1 to agree with the dialog, which comes up with "Below
    ;; the line" already selected.
    (setq note "TYP." below 1 result 0)

    (if (setq path (DimNote:WriteDcl))
        (if (< (setq id (load_dialog path)) 0)
            (princ "\n** The dialog would not load. **")
            (if (not (new_dialog "dimnote" id))
                (princ "\n** The dialog would not open. **")
                (progn
                    (start_list "lb_notes")
                    (mapcar 'add_list DimNote:Notes)
                    (end_list)

                    (set_tile "eb_note" note)
                    (set_tile "lb_notes" "0")
                    (set_tile "dn_below" "1")

                    ;; Picking from the list fills the edit box, which can then
                    ;; be typed over - the list is a shortcut, not a cage.
                    (action_tile "lb_notes"
                        "(set_tile \"eb_note\" (nth (atoi $value) DimNote:Notes))")
                    (action_tile "dn_beside" "(setq below 0)")
                    (action_tile "dn_below"  "(setq below 1)")
                    (action_tile "accept"
                        (strcat "(if (= \"\" (DimNote:Squash (get_tile \"eb_note\")))"
                                "  (set_tile \"error\" \"Type a note first.\")"
                                "  (progn (setq note (get_tile \"eb_note\"))"
                                "         (done_dialog 1)))"))
                    (action_tile "cancel" "(done_dialog 0)")

                    (setq result (start_dialog))))))

    ;; Let the dialog go before selecting, so it is not sitting over the drawing
    ;; while you pick. The undo group stays open until the very end, so the whole
    ;; run - however many dimensions get noted - is one U.
    (if id (unload_dialog id))
    (setq id nil)
    (if (and path (findfile path)) (vl-file-delete path))
    (setq path nil)

    (if (= result 1)
        (progn
            (while (setq ent (DimNote:PickDim "Dimension to note" nil))
                (setq data (entget ent)
                      old  (cdr (assoc 1 data)))
                ;; An empty override means "just the measurement". Turn it into
                ;; <> so text can be added without losing the live value.
                (if (= old "") (setq old "<>"))
                (setq new
                    (if (zerop below)
                        (strcat old " " note)
                        ;; A second \X would be ignored, so once the text is
                        ;; already split, extra lines go on with \P.
                        (if (wcmatch old "*\\X*")
                            (strcat old "\\P" note)
                            (strcat old "\\X" note))))
                (entmod (subst (cons 1 new) (assoc 1 data) data))
                (entupd ent))))

    (DimNote:Restore)
    (princ)
)

;;; Strip spaces from both ends - used to decide whether the note field is
;;; really empty. Defined outside the command because ACTION_TILE expressions
;;; are evaluated in the top-level namespace.
(defun DimNote:Squash ( s )
    (while (and (> (strlen s) 0) (= " " (substr s 1 1)))
        (setq s (substr s 2)))
    (while (and (> (strlen s) 0) (= " " (substr s (strlen s) 1)))
        (setq s (substr s 1 (1- (strlen s)))))
    s
)

;;; ---------------------------------------------------------------------------
;;; DIMFLIP
;;; ---------------------------------------------------------------------------

(defun c:DIMFLIP ( / *error* vars vals ent data p13 p14 n )

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

    (defun DimFlip: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 )
        (DimFlip:Restore)
        (if (and msg (not (wcmatch (strcase msg t) "*break*,*cancel*,*exit*")))
            (princ (strcat "\n** DIMFLIP error: " msg " **")))
        (princ)
    )

    (setvar "CMDECHO" 0)
    (vl-catch-all-apply '(lambda ( ) (*push-error-using-command*)) '())
    (command "_.UNDO" "_Begin")

    (setq n 0)
    ;; Groups 13 and 14 are the two definition points - where the extension
    ;; lines start. Swapping them reverses the direction the dimension is
    ;; measured in, which is what puts the text and arrows on the other side.
    (while (setq ent (DimNote:PickDim "Dimension to flip" t))
        (setq data (entget ent)
              p13  (cdr (assoc 13 data))
              p14  (cdr (assoc 14 data)))
        (if (and p13 p14)
            (progn
                (entmod (subst (cons 14 p13) (assoc 14 data)
                        (subst (cons 13 p14) (assoc 13 data) data)))
                (entupd ent)
                (setq n (1+ n)))
            (princ "\n  That dimension has no definition points to swap.")))

    (princ (strcat "\n" (itoa n) " dimension" (if (= n 1) "" "s") " flipped."))

    (DimFlip:Restore)
    (princ)
)

(princ)
