;;; ---------------------------------------------------------------------------
;;; CabinetFace.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; PARAMETRIC CABINET ELEVATIONS
;;;
;;; PURPOSE
;;;   Draws the front elevation of a cabinet - carcase outline, base trim, and a
;;;   full run of drawer or door panels - from a handful of dimensions. Three
;;;   types are built in:
;;;
;;;     Base   floor-standing, sitting on a base trim, with a stack of drawers
;;;     Linen  the same but with a tall door panel above the drawer stack
;;;     Wall   hung, no base trim, a single door panel filling the face
;;;
;;;   This is the drawing nobody wants to do by hand. A six-drawer bank is thirty
;;;   rectangles and twenty-four mitre lines, every one of which has to divide
;;;   evenly into the carcase or the elevation looks wrong.
;;;
;;; HOW IT WORKS
;;;   1. Every panel on a cabinet face is the same thing: an outer rectangle, an
;;;      inner rectangle inset by the frame width, and four mitre lines joining
;;;      their corners. That is drawn by one function, called for every drawer,
;;;      every filler and every door. Get it right once and the whole elevation
;;;      is consistent.
;;;
;;;   2. Drawer height is DERIVED, not asked for. You give the drawer
;;;      configuration - how many drawers the face is divided into - and the
;;;      height falls out of the space available:
;;;
;;;        usable height  =  carcase height - 2 x edge offset - base trim
;;;        drawer height  =  (usable height - gaps between drawers) / count
;;;
;;;      That is what makes the stack always divide evenly.
;;;
;;;   3. CONFIGURATION versus NUMBER of drawers. These are deliberately separate.
;;;      The configuration sets the module - divide the face into four and every
;;;      drawer is a quarter-height. The number says how many of those four are
;;;      actually drawers. Ask for 4 and 2 and you get two drawers at quarter
;;;      height with a half-height filler panel below, which is exactly how a
;;;      sink base or an appliance surround is drawn. Ask for 4 and 4 and you get
;;;      a four-drawer bank.
;;;
;;;   4. WIDE CABINETS SPLIT AUTOMATICALLY. A drawer wider than 24 inches is not
;;;      buildable, so if the calculated width exceeds that the face is divided
;;;      into as many side-by-side banks as it takes to bring it back under, and
;;;      the whole stack is repeated across. You do not have to work out how many
;;;      banks a 60 inch cabinet needs - it comes out as three.
;;;
;;;   5. Everything is drawn in place at the point you pick, which is the BOTTOM
;;;      LEFT corner of the carcase for base and linen cabinets and the TOP LEFT
;;;      for wall cabinets - the corners you would actually dimension from.
;;;
;;; ALL DIMENSIONS ARE REMEMBERED
;;;   Every value you enter becomes the default for the next cabinet, so drawing
;;;   a run of kitchen units means changing only the width each time.
;;;
;;;   CABINETFACE  - draw a cabinet front elevation
;;; ---------------------------------------------------------------------------

;;; ---------------------------------------------------------------------------
;;; SESSION MEMORY
;;;
;;; Defaults are in inches, matching normal cabinet practice. They persist for
;;; the drawing session so a run of units is quick to lay out.
;;; ---------------------------------------------------------------------------

(if (null *CabinetFace:Prefs*)
    (setq *CabinetFace:Prefs*
        (list (cons "WIDTH"  24.0)   ; carcase width
              (cons "HEIGHT" 36.0)   ; carcase height, base and wall
              (cons "LINENH" 72.0)   ; carcase height when it is a linen unit
              (cons "TRIMH"   4.0)   ; base trim height
              (cons "TRIMO"   1.0)   ; base trim setback
              (cons "OSET1"   2.0)   ; edge offset, panel to carcase side
              (cons "OSET2"   1.0)   ; frame width within each panel
              (cons "DOORH"  36.0)   ; door panel height on a linen unit
              (cons "CONFIG"  4)     ; how many modules the face divides into
              (cons "DRAWERS" 1)     ; how many of those modules are drawers
        )
    )
)

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

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

;;; ---------------------------------------------------------------------------
;;; DIRECTIONS
;;; Named once so the geometry below reads as up/down/left/right rather than as
;;; a scattering of pi fractions.
;;; ---------------------------------------------------------------------------

(setq CabinetFace:RIGHT 0.0
      CabinetFace:UP    (* pi 0.5)
      CabinetFace:LEFT  pi
      CabinetFace:DOWN  (* pi 1.5))

;;; ---------------------------------------------------------------------------
;;; ASKING FOR A DIMENSION
;;;
;;; One helper for every numeric prompt: shows the remembered value as the
;;; default, stores whatever is entered, and returns the value either way.
;;; INITGET 6 rejects zero and negative but still allows Enter to take the
;;; default, which is the behaviour every one of these prompts wants.
;;; ---------------------------------------------------------------------------

(defun CabinetFace:AskDist ( key prompt / v )
    (initget 6)
    (setq v (getdist (strcat "\n" prompt " <"
                             (rtos (CabinetFace:Get key) 2 3) ">: ")))
    (if v (CabinetFace:Put key v) (CabinetFace:Get key))
)

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

;;; ---------------------------------------------------------------------------
;;; THE PANEL
;;;
;;; Draws one cabinet panel: outer rectangle, inner rectangle inset by the frame
;;; width, and the four mitre lines joining their corners. This one function
;;; draws every drawer front, every filler and every door in the elevation.
;;;
;;;   tl     top-left corner of the panel
;;;   w, h   panel width and height
;;;   inset  frame width between the outer and inner rectangles
;;;
;;; A panel too small to take the inset is drawn as a plain rectangle rather than
;;; producing an inverted inner rectangle, which is what happens if the frame is
;;; wider than half the panel.
;;; ---------------------------------------------------------------------------

(defun CabinetFace:Panel ( tl w h inset / p1 p2 p3 p4 i1 i2 i3 i4 iw ih )

    (setq p1 tl
          p2 (polar p1 CabinetFace:RIGHT w)
          p3 (polar p2 CabinetFace:DOWN  h)
          p4 (polar p3 CabinetFace:LEFT  w))

    (command "_.PLINE" p1 "_W" 0 0 p2 p3 p4 "_C")

    (setq iw (- w (* inset 2.0))
          ih (- h (* inset 2.0)))

    (if (and (> iw 0.0) (> ih 0.0))
        (progn
            (setq i1 (polar (polar p1 CabinetFace:RIGHT inset) CabinetFace:DOWN inset)
                  i2 (polar i1 CabinetFace:RIGHT iw)
                  i3 (polar i2 CabinetFace:DOWN  ih)
                  i4 (polar i3 CabinetFace:LEFT  iw))

            (command "_.PLINE" i1 "_W" 0 0 i2 i3 i4 "_C")

            ;; The four mitres. Without these the panel reads as two loose
            ;; rectangles rather than as a framed front.
            (command "_.LINE" p1 i1 "")
            (command "_.LINE" p2 i2 "")
            (command "_.LINE" p3 i3 "")
            (command "_.LINE" p4 i4 "")
        )
    )
    (princ)
)

;;; ---------------------------------------------------------------------------
;;; ONE STACK OF DRAWERS
;;;
;;; Draws `count` drawers of height `dh` down from the top-left point given, then
;;; the filler panel underneath if the configuration leaves room for one.
;;; ---------------------------------------------------------------------------

(defun CabinetFace:Stack ( tl w dh gap inset count config fillH / y )
    (setq y 0.0)
    (repeat count
        (CabinetFace:Panel (polar tl CabinetFace:DOWN y) w dh inset)
        (setq y (+ y dh gap)))

    ;; Anything left over below the drawers is a fixed panel - a sink front or
    ;; an appliance surround.
    (if (and (< count config) (> fillH 0.0))
        (CabinetFace:Panel (polar tl CabinetFace:DOWN y) w fillH inset))
    (princ)
)

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

(defun c:CABINETFACE ( / *error* vars vals kind base
                         cw ch trimH trimO os1 os2 doorH config drawers
                         usableW usableH dh fillH banks bankW
                         inner top i n )

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

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

    (initget 1 "Base Linen Wall")
    (setq kind (getkword "\nCabinet type [Base/Linen/Wall] <Base>: "))
    (if (null kind) (setq kind "Base"))

    ;; --- dimensions ---------------------------------------------------------
    (setq cw (CabinetFace:AskDist "WIDTH" "Cabinet width"))

    ;; A linen unit keeps its own height default, because 72 inches is nothing
    ;; like the 36 a base unit wants and sharing one default makes both wrong.
    (setq ch (if (= kind "Linen")
                 (CabinetFace:AskDist "LINENH" "Cabinet height")
                 (CabinetFace:AskDist "HEIGHT" "Cabinet height")))

    ;; A wall unit is hung, so it has no base trim to ask about.
    (if (= kind "Wall")
        (setq trimH 0.0 trimO 0.0)
        (setq trimH (CabinetFace:AskDist "TRIMH" "Base trim height")
              trimO (CabinetFace:AskDist "TRIMO" "Base trim setback")))

    (setq os1 (CabinetFace:AskDist "OSET1" "Edge offset, panel to carcase side")
          os2 (CabinetFace:AskDist "OSET2" "Frame width within each panel"))

    (if (= kind "Linen")
        (setq doorH (CabinetFace:AskDist "DOORH" "Door panel height above the drawers")))

    (if (= kind "Wall")
        (setq config 1 drawers 1)
        (setq config  (CabinetFace:AskInt "CONFIG"  "Drawer configuration, modules the face divides into")
              drawers (CabinetFace:AskInt "DRAWERS" "Number of those modules that are drawers")))

    ;; --- work out the geometry before asking where to put it ----------------
    ;; Getting this wrong should cost the user a prompt, not a pick and an undo.
    (setq usableW (- cw (* os1 2.0))
          usableH (- ch (* os1 2.0) trimH))

    ;; A drawer wider than 24 inches is not buildable, so split the face into
    ;; as many side-by-side banks as it takes to bring the width back under.
    (setq banks 1 bankW usableW)
    (while (and (> bankW 24.0) (< banks 12))
        (setq banks (1+ banks)
              bankW (/ (- cw (* os1 (1+ banks))) (float banks))))

    ;; Drawer height: the usable height less the gaps between modules, divided
    ;; by the module count. A linen unit gives up the door height first.
    (if (= kind "Linen")
        (setq dh (/ (- usableH doorH (* os1 config)) (float config)))
        (setq dh (/ (- usableH (* os1 (1- config))) (float config))))

    ;; Whatever the unused modules add up to, plus the gaps between them.
    (setq fillH (+ (* dh (- config drawers))
                   (* os1 (1- (- config drawers)))))

    (cond
        ((> drawers config)
         (princ "\n** More drawers than the configuration allows - nothing drawn. **"))

        ((and (/= kind "Wall") (<= dh 0.0))
         (princ (strcat "\n** That will not fit: the drawers work out at "
                        (rtos dh 2 3) " high."
                        "\n   Increase the cabinet height, or reduce the"
                        " configuration count or the offsets. **")))

        ((<= bankW 0.0)
         (princ "\n** The edge offset is too large for that cabinet width. **"))

        (t
            (if (> banks 1)
                (princ (strcat "\nFace split into " (itoa banks)
                               " banks of " (rtos bankW 2 3) " to keep drawers"
                               " under 24.")))

            (setvar "BLIPMODE" 0)
            (setq base (getpoint
                           (strcat "\n" (if (= kind "Wall") "Top" "Bottom")
                                   " left corner of the cabinet: ")))

            (if (null base)
                (princ "\nCancelled.")
                (progn
                    (setvar "OSMODE" 0)

                    ;; --- carcase outline ---------------------------------
                    ;; Wall units are picked at the top left and run down;
                    ;; everything else is picked at the bottom left and runs up.
                    (if (= kind "Wall")
                        (command "_.PLINE" base "_W" 0 0
                                 (polar base CabinetFace:RIGHT cw)
                                 (polar (polar base CabinetFace:RIGHT cw)
                                        CabinetFace:DOWN ch)
                                 (polar base CabinetFace:DOWN ch) "_C")
                        (command "_.PLINE" base "_W" 0 0
                                 (polar base CabinetFace:UP ch)
                                 (polar (polar base CabinetFace:UP ch)
                                        CabinetFace:RIGHT cw)
                                 (polar base CabinetFace:RIGHT cw) "_C"))

                    ;; --- base trim ---------------------------------------
                    (if (> trimH 0.0)
                        (progn
                            (command "_.LINE" (polar base CabinetFace:UP trimH)
                                     (polar (polar base CabinetFace:UP trimH)
                                            CabinetFace:RIGHT cw) "")
                            (command "_.LINE" (polar base CabinetFace:UP (- trimH trimO))
                                     (polar (polar base CabinetFace:UP (- trimH trimO))
                                            CabinetFace:RIGHT cw) "")))

                    ;; --- top left of the panel zone ----------------------
                    ;; Inset from the carcase by the edge offset on both axes.
                    (setq top (if (= kind "Wall") base (polar base CabinetFace:UP ch))
                          inner (polar (polar top CabinetFace:RIGHT os1)
                                       CabinetFace:DOWN os1))

                    (cond
                        ;; A wall unit is one door filling the whole face.
                        ((= kind "Wall")
                         (CabinetFace:Panel inner usableW
                                            (- ch (* os1 2.0)) os2))

                        (t
                            ;; Every bank across the face gets the same stack,
                            ;; stepped right by one bank width plus a gap.
                            (setq i 0)
                            (repeat banks
                                (setq n (polar inner CabinetFace:RIGHT
                                               (* i (+ bankW os1))))

                                ;; A linen unit carries its door above the
                                ;; drawers, and the stack starts below it.
                                (if (= kind "Linen")
                                    (progn
                                        (CabinetFace:Panel n bankW doorH os2)
                                        (setq n (polar n CabinetFace:DOWN
                                                       (+ doorH os1)))))

                                (CabinetFace:Stack n bankW dh os1 os2
                                                   drawers config fillH)
                                (setq i (1+ i)))
                        )
                    )

                    (princ (strcat "\n" kind " cabinet drawn - "
                                   (rtos cw 2 3) " wide x " (rtos ch 2 3) " high"
                                   (if (= kind "Wall") ""
                                       (strcat ", " (itoa drawers) " drawer(s) at "
                                               (rtos dh 2 3) " high"))
                                   "."))
                )
            )
        )
    )

    (CabinetFace:Restore)
    (princ)
)

(princ)
