;;; ---------------------------------------------------------------------------
;;; WindowSymbol.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; DOUBLE-HUNG WINDOW SYMBOLS - ELEVATION AND PLAN
;;;
;;; PURPOSE
;;;   Draws a double-hung window two ways:
;;;
;;;     WINDOWELEV  the elevation - outer casing, frame, sill, meeting rail,
;;;                 both sashes and any number of glazing bars
;;;     WINDOWPLAN  the plan - cuts the opening through the wall, draws the
;;;                 jambs, the glass line and the sill projection
;;;
;;; HOW THE ELEVATION IS BUILT
;;;   Everything is measured from the frame opening - the hole the window sits
;;;   in - because that is the dimension an architect actually specifies and the
;;;   one a builder sets out. Give it either as two corners or as a corner plus a
;;;   width and height.
;;;
;;;   From that opening, four things are offset:
;;;
;;;     the outer casing, out by the casing width in both directions
;;;     the sill, projecting past the opening at the bottom
;;;     the meeting rail, at half the opening height
;;;     the glass line of each sash, in by the sash rail width
;;;
;;;   Glazing bars then divide each sash independently, so a window with two
;;;   panes across and three rows gets six lights per sash, twelve in total.
;;;
;;; HOW THE PLAN IS BUILT
;;;   You give the centre of the window on the INSIDE wall face, then a point on
;;;   the outside face. That second pick does two jobs at once: it fixes the wall
;;;   thickness, and the direction between the two points establishes which way
;;;   the wall faces - so the window comes out square to the wall at any angle,
;;;   with no need to tell it the rotation.
;;;
;;;   The wall is then broken at the two jambs and the opening closed off.
;;;
;;; WHAT WAS REMOVED
;;;   The original loaded its own menu file and drove the screen menu on every
;;;   run, restoring the previous menu on the way out. Screen menus have not
;;;   existed in AutoCAD for many releases, and swapping a user's menu underneath
;;;   them was disruptive even when they did. All of it is gone; the prompts do
;;;   the same job.
;;;
;;;   WINDOWELEV  - draw a double-hung window in elevation
;;;   WINDOWPLAN  - cut a window opening through a wall and draw it in plan
;;; ---------------------------------------------------------------------------

;;; ---------------------------------------------------------------------------
;;; SESSION MEMORY
;;;
;;; Sizes are real-world dimensions in drawing units, NOT scaled by DIMSCALE -
;;; a 1.5 inch casing is 1.5 inches whatever the plot scale, and scaling it would
;;; make the window the wrong size.
;;; ---------------------------------------------------------------------------

(if (null *WinSym:Prefs*)
    (setq *WinSym:Prefs*
        (list (cons "WIDTH"    36.0)   ; frame opening width
              (cons "HEIGHT"   60.0)   ; frame opening height
              (cons "ROWS"      2)     ; glazing bar rows per sash
              (cons "PANES"     3)     ; panes across each sash
              (cons "CASING"    1.5)   ; outer casing, beyond the opening
              (cons "SASH"      2.0)   ; sash rail width, opening to glass
              (cons "SILLPROJ"  2.0)   ; sill projection past the opening
              (cons "SILLDROP"  3.5)   ; sill depth below the opening
              (cons "HALFWIDTH" 24.0)  ; plan half-width
              (cons "BREAKWALL" "Yes") ; cut the opening through the wall
              (cons "BARS"      "Yes") ; draw glazing bars
        )
    )
)

(defun WinSym:Get ( key ) (cdr (assoc key *WinSym:Prefs*)))

(defun WinSym:Put ( key val )
    (setq *WinSym:Prefs*
        (cons (cons key val)
              (vl-remove-if '(lambda (p) (= (car p) key)) *WinSym:Prefs*)))
    val
)

;;; ---------------------------------------------------------------------------
;;; HELPERS
;;; ---------------------------------------------------------------------------

(defun WinSym:Layer ( name )
    (if (tblsearch "layer" name)
        (command "_.LAYER" "_ON" name "_THAW" name "_UNLOCK" name "")
        (command "_.LAYER" "_NEW" name ""))
    (setvar "CLAYER" name)
)

(defun WinSym:Line ( pts close )
    (command "_.LINE")
    (foreach p pts (command p))
    (if close (command "_C") (command ""))
    (princ)
)

;;; Offset a point by separate x and y amounts. Used all through the elevation,
;;; which is drawn square - the plan uses POLAR instead because it works at any
;;; wall angle.
(defun WinSym:XY ( p dx dy )
    (list (+ (car p) dx) (+ (cadr p) dy) (if (caddr p) (caddr p) 0.0))
)

;;; Ask for a distance with a remembered default.
(defun WinSym:AskDist ( key prompt / v )
    (initget 6)
    (setq v (getdist (strcat "\n" prompt " <" (rtos (WinSym:Get key) 2 2) ">: ")))
    (if v (WinSym:Put key v) (WinSym:Get key))
)

(defun WinSym:AskInt ( key prompt / v )
    (initget 6)
    (setq v (getint (strcat "\n" prompt " <" (itoa (WinSym:Get key)) ">: ")))
    (if v (WinSym:Put key v) (WinSym:Get key))
)

;;; ---------------------------------------------------------------------------
;;; GLAZING BARS
;;;
;;; Divides one sash into a grid. Takes the two corners of that sash's glass and
;;; draws the internal dividers only - the surrounding frame is already there, so
;;; a 3-pane sash needs 2 bars, not 4.
;;; ---------------------------------------------------------------------------

(defun WinSym:Bars ( lowerLeft upperRight across rows / w h i x y )
    (setq w (- (car  upperRight) (car  lowerLeft))
          h (- (cadr upperRight) (cadr lowerLeft)))

    ;; Vertical bars.
    (setq i 1)
    (while (< i across)
        (setq x (+ (car lowerLeft) (* w (/ (float i) across))))
        (WinSym:Line (list (list x (cadr lowerLeft))
                           (list x (cadr upperRight))) nil)
        (setq i (1+ i)))

    ;; Horizontal bars.
    (setq i 1)
    (while (< i rows)
        (setq y (+ (cadr lowerLeft) (* h (/ (float i) rows))))
        (WinSym:Line (list (list (car lowerLeft)  y)
                           (list (car upperRight) y)) nil)
        (setq i (1+ i)))
    (princ)
)

;;; ---------------------------------------------------------------------------
;;; ELEVATION
;;; ---------------------------------------------------------------------------

(defun c:WINDOWELEV ( / *error* vars vals mode ll ur w h
                        casing sash proj drop bars rows across
                        ll2 ur2 lr ul mid1 mid2
                        upLL upUR loLL loUR )

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

    (defun WinSym: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 )
        (WinSym:Restore)
        (if (and msg (not (wcmatch (strcase msg t) "*break*,*cancel*,*exit*")))
            (princ (strcat "\n** WINDOWELEV 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. The declaring call is absent on older
    ;; releases, so it is wrapped rather than tested for.
    (vl-catch-all-apply '(lambda ( ) (*push-error-using-command*)) '())
    (command "_.UNDO" "_Begin")
    (setvar "BLIPMODE" 0)

    (initget "Box Distance")
    (setq mode (getkword "\nSize the opening by [Box/Distance] <Distance>: "))
    (if (null mode) (setq mode "Distance"))

    (setq ll (getpoint "\nInside lower-left corner of the frame opening: "))

    (cond
        ((null ll) (princ "\nCancelled."))

        (t
            (if (= mode "Box")
                (setq ur (getcorner ll "\nInside upper-right corner: "))
                (progn
                    (setq w (WinSym:AskDist "WIDTH"  "Opening width")
                          h (WinSym:AskDist "HEIGHT" "Opening height"))
                    (setq ur (WinSym:XY ll w h))))

            (if (null ur)
                (princ "\nCancelled.")
                (progn
                    (setq w (- (car  ur) (car  ll))
                          h (- (cadr ur) (cadr ll)))

                    (initget "Yes No")
                    (setq bars (getkword (strcat "\nDraw glazing bars [Yes/No] <"
                                                 (WinSym:Get "BARS") ">: ")))
                    (if (null bars) (setq bars (WinSym:Get "BARS")))
                    (WinSym:Put "BARS" bars)

                    (if (= bars "Yes")
                        (setq rows   (WinSym:AskInt "ROWS"  "Glazing bar rows per sash")
                              across (WinSym:AskInt "PANES" "Panes across each sash")))

                    (setq casing (WinSym:Get "CASING")
                          sash   (WinSym:Get "SASH")
                          proj   (WinSym:Get "SILLPROJ")
                          drop   (WinSym:Get "SILLDROP"))

                    (setvar "OSMODE" 0)
                    (WinSym:Layer "WINDOW-SYMBOL")

                    ;; --- frame opening and outer casing --------------------
                    (setq lr (WinSym:XY ur 0 (- h))
                          ul (WinSym:XY ll 0 h))
                    (WinSym:Line (list ll lr ur ul) t)

                    (setq ll2 (WinSym:XY ll (- casing) (- casing))
                          ur2 (WinSym:XY ur casing casing))
                    (WinSym:Line (list ll2
                                       (WinSym:XY ur2 0 (- (+ h (* casing 2))))
                                       ur2
                                       (WinSym:XY ll2 0 (+ h (* casing 2)))) t)

                    ;; --- sill ---------------------------------------------
                    ;; Steps out from each jamb then drops, so it reads as a
                    ;; projecting sill rather than a line under the window.
                    (WinSym:Line
                        (list (WinSym:XY ll (- proj) 0)
                              (WinSym:XY ll (- (+ proj drop)) 0)
                              (WinSym:XY ll (- (+ proj drop)) (- drop))
                              (WinSym:XY lr (+ proj drop) (- drop))
                              (WinSym:XY lr (+ proj drop) 0)
                              (WinSym:XY lr proj 0))
                        nil)

                    ;; --- meeting rail, where the two sashes overlap --------
                    (setq mid1 (WinSym:XY ll 0 (/ h 2.0))
                          mid2 (WinSym:XY lr 0 (/ h 2.0)))
                    (WinSym:Line (list mid1 mid2) nil)

                    ;; --- the two sashes -----------------------------------
                    ;; Each glass line is inset from the opening on three sides
                    ;; and from the meeting rail on the fourth.
                    (setq upLL (WinSym:XY mid1 sash sash)
                          upUR (WinSym:XY ur (- sash) (- sash))
                          loLL (WinSym:XY ll sash sash)
                          loUR (WinSym:XY mid2 (- sash) (- sash)))

                    (WinSym:Line (list upLL
                                       (list (car upUR) (cadr upLL))
                                       upUR
                                       (list (car upLL) (cadr upUR))) t)
                    (WinSym:Line (list loLL
                                       (list (car loUR) (cadr loLL))
                                       loUR
                                       (list (car loLL) (cadr loUR))) t)

                    (if (and (= bars "Yes") (> across 0) (> rows 0))
                        (progn
                            (WinSym:Bars upLL upUR across rows)
                            (WinSym:Bars loLL loUR across rows)))

                    (princ (strcat "\nWindow drawn - opening " (rtos w 2 2)
                                   " x " (rtos h 2 2)
                                   (if (= bars "Yes")
                                       (strcat ", " (itoa (* across rows))
                                               " lights per sash")
                                       "")
                                   "."))
                )
            )
        )
    )

    (WinSym:Restore)
    (princ)
)

;;; ---------------------------------------------------------------------------
;;; PLAN
;;; ---------------------------------------------------------------------------

(defun c:WINDOWPLAN ( / *error* vars vals inPt outPt half brk
                        face left right thk halfThk
                        w1 w2 w3 w4 g1 g2 s1 s2 s3 s4 )

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

    (defun WinSym: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 )
        (WinSym:Restore)
        (if (and msg (not (wcmatch (strcase msg t) "*break*,*cancel*,*exit*")))
            (princ (strcat "\n** WINDOWPLAN 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. The declaring call is absent on older
    ;; releases, so it is wrapped rather than tested for.
    (vl-catch-all-apply '(lambda ( ) (*push-error-using-command*)) '())
    (command "_.UNDO" "_Begin")
    (setvar "BLIPMODE" 0)

    (setq inPt (getpoint "\nCentre of the window on the INSIDE wall face: "))

    (if (null inPt)
        (princ "\nCancelled.")
        (progn
            ;; Perpendicular osnap onto the outside face gives the wall thickness
            ;; and the wall direction in one pick.
            (setvar "OSMODE" 128)
            (setq outPt (getpoint inPt "\nA point on the OUTSIDE wall face: "))
            (setvar "OSMODE" 0)

            (cond
                ((null outPt) (princ "\nCancelled."))

                ((< (distance inPt outPt) 1e-6)
                 (princ "\n** Those two points are the same - the wall has no thickness. **"))

                (t
                    (setq half (WinSym:AskDist "HALFWIDTH" "Half-width of the window"))

                    (initget "Yes No")
                    (setq brk (getkword (strcat "\nCut the opening through the wall [Yes/No] <"
                                                (WinSym:Get "BREAKWALL") ">: ")))
                    (if (null brk) (setq brk (WinSym:Get "BREAKWALL")))
                    (WinSym:Put "BREAKWALL" brk)

                    ;; The direction from inside to outside IS the wall normal,
                    ;; so the window squares itself to the wall at any angle
                    ;; without ever asking for a rotation.
                    (setq face     (angle inPt outPt)
                          left     (- face (/ pi 2.0))
                          right    (+ face (/ pi 2.0))
                          thk      (distance inPt outPt)
                          halfThk  (/ thk 2.0))

                    (setq w1 (polar inPt  left  half)   ; inside jambs
                          w2 (polar inPt  right half)
                          w3 (polar outPt left  half)   ; outside jambs
                          w4 (polar outPt right half)
                          g1 (polar w1 face halfThk)    ; glass line, mid-wall
                          g2 (polar w2 face halfThk))

                    ;; Sill returns, projecting past the opening outside.
                    (setq s1 (polar outPt left  (+ half 1.5))
                          s4 (polar outPt right (+ half 1.5))
                          s2 (polar s1 face 1.5)
                          s3 (polar s4 face 1.5))

                    (if (= brk "Yes")
                        (progn
                            ;; BREAK takes its object selection point as the
                            ;; first break point, so one pair of points does both
                            ;; jobs on each wall face.
                            (vl-catch-all-apply
                                '(lambda ( ) (command "_.BREAK" w1 w1 w2)))
                            (vl-catch-all-apply
                                '(lambda ( ) (command "_.BREAK" w3 w3 w4)))))

                    (WinSym:Layer "WINDOW-SYMBOL")
                    (WinSym:Line (list w1 w3) nil)     ; jamb returns
                    (WinSym:Line (list w2 w4) nil)
                    (WinSym:Line (list g1 g2) nil)     ; glass
                    (WinSym:Line (list w3 w4) nil)     ; outside face of opening
                    (WinSym:Line (list s1 s2 s3 s4) nil)

                    (princ (strcat "\nWindow drawn in plan - "
                                   (rtos (* half 2.0) 2 2) " wide in a "
                                   (rtos thk 2 2) " wall."))
                )
            )
        )
    )

    (WinSym:Restore)
    (princ)
)

(princ)
