;;; ---------------------------------------------------------------------------
;;; BattInsul.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; FIBREGLASS BATT INSULATION SYMBOL
;;;
;;; PURPOSE
;;;   Fills a cavity with the standard batt insulation squiggle - the serpentine
;;;   loop run used on wall sections, roof build-ups and partition plans to show
;;;   what is insulated.
;;;
;;;   Pick along one face of the cavity, give the thickness or pick the opposite
;;;   face, and the run is filled between the two.
;;;
;;; HOW IT IS DRAWN
;;;   One closed loop of polyline, not a row of inserted blocks. The centre line
;;;   runs the length of the cavity alternating between the two faces, with each
;;;   segment bulged into a half circle, which gives the familiar row of lobes.
;;;   A second pass back along the run closes it, so the batt reads as a single
;;;   object that can be selected, moved or erased in one go.
;;;
;;;   The lobes are inset slightly from the cavity faces. That gap is deliberate
;;;   - at plot scale the outer loops would otherwise merge into the wall lines
;;;   and the whole thing turns into a smudge. The original called it a pen
;;;   allowance, which is exactly what it is.
;;;
;;; ABOUT THE BULGE
;;;   A polyline segment carries a bulge: the tangent of a quarter of the arc's
;;;   included angle. A bulge of 1 is therefore a half circle, and its sign says
;;;   which way the arc turns. Alternating the sign segment by segment is what
;;;   produces the serpentine.
;;;
;;; WHAT WAS FIXED
;;;   - It could not run without BATT.DWG sitting in the current folder, and a
;;;     second file, INPUT.LSP, for one of its input helpers. Both are gone; the
;;;     geometry is drawn directly and everything needed is in this file.
;;;   - One helper was written (defun distget (b p d / t) - declaring T, the
;;;     symbol for true, as a local variable and then assigning to it. Anything
;;;     called while that function was running saw a broken T.
;;;   - Every variable in the command itself was global, including single
;;;     letters n and i.
;;;   - Insertion points were passed by writing to the LASTPOINT system variable
;;;     and reading it back between calls, so any command run in between - or
;;;     any object snap - moved the batt.
;;;   - CMDECHO was saved into a global called echo, which any other routine
;;;     using the same well-worn name would overwrite.
;;;   - No undo group, so removing a run of batt took one U per lobe.
;;;
;;;   BATT  - fill a cavity with batt insulation
;;; ---------------------------------------------------------------------------

;;; Remembered between runs - a section drawing usually wants the same thickness
;;; several times over.
(setq *Batt:Thickness* nil)

(setq Batt:INSET 0.08)   ; fraction of the thickness kept clear of each face

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

(defun Batt: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
)

;;; A polyline from a list of (point . bulge) pairs.
(defun Batt:Poly ( pairs layer closed )
    (entmake (append
        (list '(0 . "LWPOLYLINE") '(100 . "AcDbEntity") (cons 8 layer)
              '(100 . "AcDbPolyline") (cons 90 (length pairs))
              (cons 70 (if closed 1 0)))
        (apply 'append
            (mapcar '(lambda ( pr )
                        (list (cons 10 (list (car (car pr)) (cadr (car pr))))
                              (cons 42 (cdr pr))))
                    pairs))))
)

;;; ---------------------------------------------------------------------------
;;; THE BATT RUN
;;;
;;; START is where the run begins on the first face, ANG the direction along the
;;; cavity, LEN how far it runs, and WIDTH the cavity thickness. SIDE is +1 or
;;; -1 saying which way the opposite face lies.
;;; ---------------------------------------------------------------------------

(defun Batt:Draw ( start ang len width side layer / w base perp
                    step over n h i pairs sgn p )

    (setq perp (+ ang (* side (/ pi 2.0)))
          ;; How far from the centre line the arcs may reach. The inset is what
          ;; keeps the outer loops off the wall lines at plot scale.
          w    (- (/ width 2.0) (* width Batt:INSET))
          ;; Centre line of the cavity.
          base (polar start perp (/ width 2.0)))

    ;; --- sizing -------------------------------------------------------------
    ;; Each segment is a half circle on the chord between successive vertices,
    ;; so its radius is half that chord and it reaches exactly that far from the
    ;; chord's midpoint. For the loops to touch the faces without crossing them:
    ;;
    ;;     sqrt(step^2 + 4h^2) / 2 = w      so      h = sqrt(w^2 - step^2/4)
    ;;
    ;; where h is how far each vertex sits off the centre line. A step of about
    ;; w gives round-looking lobes.
    ;;
    ;; An end arc reaches (w - step/2) past its own vertex, at both ends, so the
    ;; whole run measures 2(w - step/2) + n*step. Setting that equal to the
    ;; length asked for fixes the step exactly, and the batt then fills the
    ;; cavity with no overhang at either end.
    (if (> len (* 2.5 w))
        (setq n    (max 2 (1+ (fix (+ 0.5 (/ (- len (* 2.0 w)) w)))))
              step (/ (- len (* 2.0 w)) (float (1- n)))
              over (- w (/ step 2.0)))
        ;; A cavity nearly as short as it is wide has no room for the reasoning
        ;; above - two lobes, filling what there is.
        (setq n 2 step (/ len 2.0) over 0.0))

    (setq h (sqrt (max 0.0 (- (* w w) (/ (* step step) 4.0))))
          pairs nil
          i 0)

    ;; --- the serpentine ------------------------------------------------------
    ;; One open polyline zigzagging between the two faces, every segment bulged
    ;; into a half circle and the bulges alternating in sign, which is what
    ;; turns the zigzag into a row of loops.
    (while (<= i n)
        (setq sgn (if (zerop (rem i 2)) 1.0 -1.0)
              p   (polar (polar base ang (+ over (* i step))) perp (* sgn h))
              pairs (cons (cons p (if (zerop (rem i 2)) -1.0 1.0)) pairs)
              i     (1+ i)))

    (Batt:Poly (reverse pairs) layer nil)
    n
)

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

(defun c:BATT ( / *error* vars vals sp ep ang len width side probe lay n v )

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

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

    ;; Object snap is left as the user has it - picking wall faces is exactly
    ;; what running snaps are for.
    (princ "\nBatt insulation. Pick along ONE face of the cavity.")
    (setq sp (getpoint "\nStart of the run: "))
    (if sp (setq ep (getpoint sp "\nEnd of the run, on the SAME face: ")))

    (if (or (null sp) (null ep))
        (princ "\nCancelled.")
        (progn
            (setq ang (angle sp ep)
                  len (distance sp ep))

            (if (< len 1e-6)
                (princ "\n** Those two points are the same. **")
                (progn
                    ;; Thickness, given as a number or by picking the far face.
                    ;; Picking is the usual way, and it also settles which side
                    ;; the cavity is on.
                    (initget 6)
                    (setq width
                        (getdist ep (strcat "\nCavity thickness, or pick the"
                                            " opposite face"
                                            (if *Batt:Thickness*
                                                (strcat " <" (rtos *Batt:Thickness* 2 3) ">")
                                                "")
                                            ": ")))
                    (if (null width) (setq width *Batt:Thickness*))

                    (if (null width)
                        (princ "\nNo thickness given.")
                        (progn
                            (setq *Batt:Thickness* width)

                            ;; Which side of the run does the cavity lie on?
                            ;; A typed thickness cannot say, so ask.
                            (setq probe (getpoint ep "\nPick anywhere on the cavity side: "))
                            (if (null probe)
                                (setq side 1.0)
                                ;; The cross product's sign says which side of
                                ;; the run direction the point falls on.
                                (setq side
                                    (if (minusp
                                            (- (* (cos ang) (- (cadr probe) (cadr sp)))
                                               (* (sin ang) (- (car probe) (car sp)))))
                                        -1.0 1.0)))

                            (setq lay (Batt:Layer "Insulation" 2))
                            (initget "Yes No")
                            (setq v (getkword "\nDraw on layer Insulation [Yes/No] <Yes>: "))
                            (if (= "No" v) (setq lay (getvar "CLAYER")))

                            (setq n (Batt:Draw sp ang len width side lay))

                            (princ (strcat "\n" (itoa n) " lobes over "
                                           (rtos len 2 3) ", cavity "
                                           (rtos width 2 3) "."))))))))

    (Batt:Restore)
    (princ)
)

(princ)
