;;; ---------------------------------------------------------------------------
;;; SideView.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; BUILDING A SIDE VIEW WHILE YOU DRAW THE PLAN
;;;
;;; PURPOSE
;;;   Draw a run in plan and get the elevation drawn at the same time, directly
;;;   below it and in line with it.
;;;
;;;   Each leg of the plan carries a bottom and a top level. As you pick your
;;;   way along, the plan line goes down as usual and the side view gets the
;;;   matching panel - bounded by those two levels and by the X positions of the
;;;   leg's two ends. Ducting, ceiling bulkheads, retaining walls, pipe racks,
;;;   roof upstands: anything where the plan is simple and the section is what
;;;   takes the time.
;;;
;;; HOW THE TWO VIEWS LINE UP
;;;   You give one number at the start: the Y value in the side view that stands
;;;   for a level of zero. Everything else follows from it - a level of 2400
;;;   lands 2400 above that line, whatever the plan is doing.
;;;
;;;   The X of each point is used unchanged, so the side view sits square under
;;;   the plan and stays there. That is the whole trick, and it is why this
;;;   beats drawing the section separately and trying to line it up afterwards.
;;;
;;; WHAT WAS FIXED
;;;   This replaces three commands that had to be run in the right order -
;;;   SVSTRT to set the datum, SVZSET to set the levels, SVLINE to draw - with
;;;   the whole thing carried between them in four global variables. Run them
;;;   out of order, or run anything else in between that used a variable called
;;;   pp or cp, and the drawing came out wrong with no warning. It is one
;;;   command now, and the state stays inside it.
;;;
;;;   SVSTRT set CMDECHO to 0 and nothing ever set it back, so the rest of the
;;;   session ran silently.
;;;
;;;   The drawing loop opened the LINE command, went away to ask for a point,
;;;   and closed the command in a later (command) call - relying on nothing
;;;   interrupting in between. Geometry is built directly now, so there is no
;;;   half-finished command to interrupt.
;;;
;;;   SIDEVIEW  - draw a plan and its elevation together
;;; ---------------------------------------------------------------------------

(setq *SideView:Datum* nil
      *SideView:Low*   nil
      *SideView:High*  nil)

;;; ---------------------------------------------------------------------------
;;; SUPPORT
;;; ---------------------------------------------------------------------------

(defun Side:Layer ( name colour )
    (if (not (tblsearch "LAYER" name))
        (entmake (list '(0 . "LAYER") '(100 . "AcDbSymbolTableRecord")
                       '(100 . "AcDbLayerTableRecord") (cons 2 name)
                       '(70 . 0) (cons 62 colour) '(6 . "Continuous"))))
    name
)

(defun Side:Line ( p1 p2 layer )
    (entmake (list '(0 . "LINE") (cons 8 layer) (cons 10 p1) (cons 11 p2)))
)

;;; The panel in the side view for one leg: a closed rectangle spanning the two
;;; X positions and the two levels.
(defun Side:Panel ( x1 x2 low high datum layer )
    (entmake (list '(0 . "LWPOLYLINE") '(100 . "AcDbEntity") (cons 8 layer)
                   '(100 . "AcDbPolyline") '(90 . 4) '(70 . 1)
                   (cons 10 (list x1 (+ datum low)))  '(42 . 0.0)
                   (cons 10 (list x2 (+ datum low)))  '(42 . 0.0)
                   (cons 10 (list x2 (+ datum high))) '(42 . 0.0)
                   (cons 10 (list x1 (+ datum high))) '(42 . 0.0)))
)

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

(defun c:SIDEVIEW ( / *error* vars vals datum p1 p2 low high v lay layp
                      n done nlow nhigh )

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

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

    (setvar "CMDECHO" 0)
    (setvar "BLIPMODE" 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")

    ;; --- the datum ---------------------------------------------------------
    (princ "\nThe side view is drawn below the plan, in line with it.")
    (setq v (getpoint "\nPick the level line for zero in the side view: "))

    (if v
        (setq datum (cadr v))
        (progn
            (initget 1)
            (setq datum (getreal (strcat "\nOr type its Y value"
                                         (if *SideView:Datum*
                                             (strcat " <" (rtos *SideView:Datum* 2 3) ">")
                                             "") ": ")))))
    (if (null datum) (setq datum *SideView:Datum*))

    (if (null datum)
        (princ "\nNo datum given.")
        (progn
            (setq *SideView:Datum* datum
                  lay  (Side:Layer "SideView-Plan" 7)
                  layp (Side:Layer "SideView-Elevation" 4))

            ;; --- levels, carried from run to run -------------------------
            (initget 1)
            (setq low (getreal (strcat "\nBottom level"
                                       (if *SideView:Low*
                                           (strcat " <" (rtos *SideView:Low* 2 3) ">")
                                           "") ": ")))
            (if (null low) (setq low *SideView:Low*))
            (initget 1)
            (setq high (getreal (strcat "\nTop level"
                                        (if *SideView:High*
                                            (strcat " <" (rtos *SideView:High* 2 3) ">")
                                            "") ": ")))
            (if (null high) (setq high *SideView:High*))

            (if (or (null low) (null high))
                (princ "\nNo levels given.")
                (progn
                    (if (> low high)
                        (progn (setq v low low high high v)
                               (princ "\n  Levels were the wrong way round - swapped.")))
                    (setq *SideView:Low* low *SideView:High* high)

                    (setq p1 (getpoint "\nStart of the run, in plan: "))

                    (if (null p1)
                        (princ "\nCancelled.")
                        (progn
                            (setq n 0 done nil)

                            (while (not done)
                                (initget "Levels")
                                (setq p2 (getpoint p1 (strcat "\nNext point [Levels]"
                                                              " <Enter to finish>: ")))
                                ;; GETPOINT with a keyword hands back a string
                                ;; if one was typed and a point list otherwise,
                                ;; so the type is checked before comparing - =
                                ;; is for numbers and strings and will not take
                                ;; a list.
                                (cond
                                    ((null p2) (setq done t))

                                    ;; Change the levels part way along, for a
                                    ;; run that steps up or down.
                                    ((and (= 'STR (type p2)) (= p2 "Levels"))
                                     (initget 1)
                                     (setq nlow (getreal (strcat "\n  Bottom level <"
                                                                 (rtos low 2 3) ">: ")))
                                     (if nlow (setq low nlow))
                                     (initget 1)
                                     (setq nhigh (getreal (strcat "\n  Top level <"
                                                                  (rtos high 2 3) ">: ")))
                                     (if nhigh (setq high nhigh))
                                     (if (> low high)
                                         (progn (setq v low low high high v)
                                                (princ "\n  Swapped - they were the wrong way round.")))
                                     (setq *SideView:Low* low *SideView:High* high))

                                    (t
                                     (Side:Line p1 p2 lay)
                                     ;; A leg with no run in X has no width in
                                     ;; the side view, so there is no panel to
                                     ;; draw - it is edge on.
                                     (if (> (abs (- (car p2) (car p1))) 1e-8)
                                         (progn
                                             (Side:Panel (car p1) (car p2)
                                                         low high datum layp)
                                             (setq n (1+ n)))
                                         (princ "\n  That leg runs straight back - nothing to show in elevation."))
                                     (setq p1 p2))))

                            (princ (strcat "\n" (itoa n) " panel"
                                           (if (= n 1) "" "s")
                                           " in the side view, levels "
                                           (rtos low 2 3) " to " (rtos high 2 3) "."))
                        )
                    )
                )
            )
        )
    )

    (Side:Restore)
    (princ)
)

(princ)
