;;; ---------------------------------------------------------------------------
;;; FloorJoist.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; I-JOIST AND STEEL BEAM IN 3D
;;;
;;; PURPOSE
;;;   Draws an I-section as real surfaces - a timber I-joist with its flanges
;;;   and web, or a steel beam - lying along the X axis from a point you pick.
;;;
;;;   Useful for a 3D framing model, for checking clearances against ductwork
;;;   and services, and for cutting a section anywhere along it.
;;;
;;;         ______________        top flange
;;;        |______________|
;;;             |    |             web
;;;             |    |
;;;         ____|____|____
;;;        |______________|        bottom flange
;;;
;;; WHAT IT IS MADE OF
;;;   Twelve 3D faces: the top and bottom of each flange, the two sides of each
;;;   flange, and the four sides of the web. That closes the shape completely,
;;;   so it hides properly and shades properly, which a set of lines never does.
;;;
;;;   The ends are left open, because a joist is nearly always cut by something
;;;   or runs into a hanger. Say so and the two end caps are added.
;;;
;;; WHAT WAS FIXED
;;;   The original drew everything TWICE - eighteen LINE objects and then
;;;   sixteen 3DFACEs over the top of them, so every edge in the model was two
;;;   objects deep. On a floor of forty joists that is over a thousand objects
;;;   that need not exist, and it makes the edges look heavy when shaded. The
;;;   faces alone define the shape; edges are an option now, off by default.
;;;
;;;   Every one of its forty-odd variables was global, including the twenty-six
;;;   point names it worked through.
;;;
;;;   It set CMDECHO to 0 and never put it back.
;;;
;;;   It defined a global helper called DTR to turn degrees into radians, then
;;;   used it only to write (dtr 0.0), (dtr 90) and (dtr 270) - three constants
;;;   that could have been written directly.
;;;
;;;   FLRJOIST  - draw an I-joist or beam in 3D
;;; ---------------------------------------------------------------------------

;;; Carried between runs - a floor is a lot of identical joists.
(setq *Joist:Length*    nil
      *Joist:WebDepth*  nil
      *Joist:WebThick*  nil
      *Joist:FlangeW*   nil
      *Joist:FlangeT*   nil)

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

(defun Joist: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 Joist:Face ( p1 p2 p3 p4 layer )
    (entmake (list '(0 . "3DFACE") (cons 8 layer)
                   (cons 10 p1) (cons 11 p2) (cons 12 p3) (cons 13 p4)))
)

;;; A rectangular box open at both ends, running along X. Four faces: bottom,
;;; top, near side, far side. Used for each flange and for the web.
(defun Joist:Box ( x1 x2 y1 y2 z1 z2 layer )
    (Joist:Face (list x1 y1 z1) (list x2 y1 z1) (list x2 y2 z1) (list x1 y2 z1) layer)
    (Joist:Face (list x1 y1 z2) (list x2 y1 z2) (list x2 y2 z2) (list x1 y2 z2) layer)
    (Joist:Face (list x1 y1 z1) (list x2 y1 z1) (list x2 y1 z2) (list x1 y1 z2) layer)
    (Joist:Face (list x1 y2 z1) (list x2 y2 z1) (list x2 y2 z2) (list x1 y2 z2) layer)
)

;;; The I-shaped end cap, as three faces - the two flange ends and the web end.
(defun Joist:Cap ( x fy1 fy2 wy1 wy2 z0 z1 z2 z3 layer )
    (Joist:Face (list x fy1 z0) (list x fy2 z0) (list x fy2 z1) (list x fy1 z1) layer)
    (Joist:Face (list x wy1 z1) (list x wy2 z1) (list x wy2 z2) (list x wy1 z2) layer)
    (Joist:Face (list x fy1 z2) (list x fy2 z2) (list x fy2 z3) (list x fy1 z3) layer)
)

(defun Joist:Ask ( prompt default / v )
    (initget 6)
    (setq v (getdist (strcat "\n" prompt
                             (if default (strcat " <" (rtos default 2 3) ">") "")
                             ": ")))
    (if v v default)
)

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

(defun c:FLRJOIST ( / *error* vars vals len wd wt fw ft base lay
                      x0 x1 fy1 fy2 wy1 wy2 z0 z1 z2 z3 caps edges v )

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

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

    (setq len (Joist:Ask "Length of the joist" *Joist:Length*)
          wd  (Joist:Ask "Web depth, between the flanges" *Joist:WebDepth*)
          wt  (Joist:Ask "Web thickness" *Joist:WebThick*)
          fw  (Joist:Ask "Flange width" *Joist:FlangeW*)
          ft  (Joist:Ask "Flange thickness" *Joist:FlangeT*))

    (if (not (and len wd wt fw ft))
        (princ "\nNot enough dimensions given.")
        (progn
            (setq *Joist:Length* len *Joist:WebDepth* wd *Joist:WebThick* wt
                  *Joist:FlangeW* fw  *Joist:FlangeT* ft)

            (if (> wt fw)
                (princ "\n** The web is wider than the flange - drawn as given. **"))

            (initget "Yes No")
            (setq caps (= "Yes" (getkword "\nCap the ends [Yes/No] <No>: ")))
            (initget "Yes No")
            (setq edges (= "Yes" (getkword "\nDraw edge lines as well [Yes/No] <No>: ")))

            (setq base (getpoint "\nBottom centre of one end: "))

            (if (null base)
                (princ "\nCancelled.")
                (progn
                    (setvar "OSMODE" 0)
                    (setq lay (Joist:Layer "Joist" 2))

                    ;; The section, worked out once. X runs along the joist, Y
                    ;; across it, Z up from the picked point.
                    (setq x0  (car base)
                          x1  (+ x0 len)
                          fy1 (- (cadr base) (/ fw 2.0))
                          fy2 (+ (cadr base) (/ fw 2.0))
                          wy1 (- (cadr base) (/ wt 2.0))
                          wy2 (+ (cadr base) (/ wt 2.0))
                          z0  (caddr base)
                          z1  (+ z0 ft)          ; top of the bottom flange
                          z2  (+ z1 wd)          ; underside of the top flange
                          z3  (+ z2 ft))         ; top of the top flange

                    (Joist:Box x0 x1 fy1 fy2 z0 z1 lay)   ; bottom flange
                    (Joist:Box x0 x1 wy1 wy2 z1 z2 lay)   ; web
                    (Joist:Box x0 x1 fy1 fy2 z2 z3 lay)   ; top flange

                    (if caps
                        (progn
                            (Joist:Cap x0 fy1 fy2 wy1 wy2 z0 z1 z2 z3 lay)
                            (Joist:Cap x1 fy1 fy2 wy1 wy2 z0 z1 z2 z3 lay)))

                    (if edges
                        (foreach e
                            (list
                                (list (list x0 fy1 z0) (list x1 fy1 z0))
                                (list (list x0 fy2 z0) (list x1 fy2 z0))
                                (list (list x0 fy1 z1) (list x1 fy1 z1))
                                (list (list x0 fy2 z1) (list x1 fy2 z1))
                                (list (list x0 wy1 z1) (list x1 wy1 z1))
                                (list (list x0 wy2 z1) (list x1 wy2 z1))
                                (list (list x0 wy1 z2) (list x1 wy1 z2))
                                (list (list x0 wy2 z2) (list x1 wy2 z2))
                                (list (list x0 fy1 z2) (list x1 fy1 z2))
                                (list (list x0 fy2 z2) (list x1 fy2 z2))
                                (list (list x0 fy1 z3) (list x1 fy1 z3))
                                (list (list x0 fy2 z3) (list x1 fy2 z3)))
                            (entmake (list '(0 . "LINE") (cons 8 lay)
                                           (cons 10 (car e)) (cons 11 (cadr e))))))

                    (princ (strcat "\nJoist " (rtos len 2 3) " long, "
                                   (rtos (- z3 z0) 2 3) " deep overall, "
                                   (itoa (+ 12 (if caps 6 0))) " faces"
                                   (if edges " plus edges" "") "."))
                )
            )
        )
    )

    (Joist:Restore)
    (princ)
)

(princ)
