;;; ---------------------------------------------------------------------------
;;; MachineFeature.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; MACHINED HOLE FEATURES IN SECTION
;;;
;;; PURPOSE
;;;   Draws the machined features that fill a mechanical detail, at any diameter,
;;;   depth and rotation:
;;;
;;;     Drilled       a plain drilled hole with its conical point
;;;     Tapped        a drilled hole with the thread envelope over it
;;;     Counterbore   a flat-bottomed recess above a drilled hole
;;;     Countersink   a conical recess above a drilled hole
;;;     Plan          a tapped hole seen from above, as two circles
;;;     Dowel         a dowel pin with chamfered ends
;;;
;;;   Plus the surface roughness symbol, which AutoCAD has never had a native
;;;   equivalent for.
;;;
;;; THE DRILL POINT
;;;   The cone at the bottom of a drilled hole is not decoration - it is where
;;;   the drill actually stops, and a detail that omits it or draws it flat will
;;;   be queried by whoever machines the part. It is drawn here at a 120 degree
;;;   included angle, which is the conventional approximation of the 118 degree
;;;   standard twist drill point and the one every drawing office uses.
;;;
;;;   That gives the point a height of radius times tan(30), which is why the
;;;   slant length below works out as radius over cos(30) - one constant,
;;;   0.866025, used in three of the six features.
;;;
;;;   Depth is measured to the FULL DIAMETER, not to the tip, which is how a
;;;   drilled depth is specified and inspected. The point is then added below it.
;;;
;;; THE TAP DRILL
;;;   A tapped hole is drawn with its tapping drill at 84% of the thread outside
;;;   diameter, which is about right for a coarse thread across the common sizes.
;;;   The thread envelope is drawn over it in hidden line, stopping one sixteenth
;;;   short of the drill depth - threads never run to the bottom of the hole
;;;   because the tap has a lead.
;;;
;;; LAYERS
;;;   Three, created on first use and left alone afterwards:
;;;     MACHFEAT-OBJECT   the cut outline
;;;     MACHFEAT-HIDDEN   thread envelopes, in HIDDEN linetype
;;;     MACHFEAT-CENTRE   hole centrelines, in CENTER linetype
;;;
;;;   MACHFEAT    - draw a machined hole feature or dowel pin
;;;   FINISHMARK  - place a surface roughness symbol
;;; ---------------------------------------------------------------------------

;;; ---------------------------------------------------------------------------
;;; SESSION MEMORY
;;; ---------------------------------------------------------------------------

(if (null *MachFeat:Prefs*)
    (setq *MachFeat:Prefs*
        (list (cons "TYPE"    "Drilled")
              (cons "DIA"     0.25)
              (cons "DEPTH"   0.75)
              (cons "CBDIA"   0.4375)   ; counterbore / countersink diameter
              (cons "CBDEPTH" 0.25)     ; counterbore depth
              (cons "LENGTH"  1.0)      ; dowel pin length
              (cons "ANG"     0.0)      ; direction the feature runs
        )
    )
)

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

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

;;; ---------------------------------------------------------------------------
;;; CONSTANTS
;;; ---------------------------------------------------------------------------

;;; cos(30 degrees). The drill point is a 120 degree cone, so its slant length is
;;; the radius divided by this, and its height is the radius times tan(30).
(setq MachFeat:COS30 0.866025)

;;; Tapping drill as a proportion of thread outside diameter.
(setq MachFeat:TAPDRILL 0.84)

;;; How far the centreline runs past each end of the hole.
(setq MachFeat:CLEXTEND 0.0625)

;;; ---------------------------------------------------------------------------
;;; LAYERS
;;; ---------------------------------------------------------------------------

(defun MachFeat:Layer ( name ltype / )
    (if (not (tblsearch "layer" name))
        (progn
            ;; The linetype has to exist before a layer can be given it, and a
            ;; missing linetype file is not worth aborting the whole command for.
            (if (and ltype (not (tblsearch "ltype" ltype)))
                (vl-catch-all-apply
                    '(lambda ( ) (command "_.-LINETYPE" "_Load" ltype "acad.lin" ""))))
            (command "_.LAYER" "_NEW" name "")
            (if (and ltype (tblsearch "ltype" ltype))
                (command "_.LAYER" "_LTYPE" ltype name "")))
        (command "_.LAYER" "_ON" name "_THAW" name "_UNLOCK" name ""))
    (setvar "CLAYER" name)
)

(defun MachFeat:Object ( ) (MachFeat:Layer "MACHFEAT-OBJECT" nil))
(defun MachFeat:Hidden ( ) (MachFeat:Layer "MACHFEAT-HIDDEN" "HIDDEN"))
(defun MachFeat:Centre ( ) (MachFeat:Layer "MACHFEAT-CENTRE" "CENTER"))

;;; ---------------------------------------------------------------------------
;;; GEOMETRY HELPERS
;;;
;;; Everything is built from a point on the hole axis plus two offsets: how far
;;; ALONG the axis, and how far ACROSS it. That single convention is what lets
;;; every feature be drawn at any rotation without a transformation anywhere.
;;; ---------------------------------------------------------------------------

(defun MachFeat:Pt ( base ang along across )
    (polar (polar base ang along) (+ ang (* pi 0.5)) across)
)

(defun MachFeat:Line ( pts )
    (command "_.LINE")
    (foreach p pts (command p))
    (command "")
    (princ)
)

;;; The three points of a drill point cone: the two shoulders at full diameter
;;; and the tip on the axis. Returned in drawing order, top shoulder first.
;;;
;;;   depth is measured to FULL DIAMETER; the tip sits below that.
(defun MachFeat:DrillPoint ( base ang rad depth / rise )
    (setq rise (/ rad MachFeat:COS30))          ; slant length of the cone
    (list (MachFeat:Pt base ang depth rad)                       ; top shoulder
          (MachFeat:Pt base ang (+ depth (* rise 0.5)) 0.0)      ; tip on the axis
          (MachFeat:Pt base ang depth (- rad)))                  ; bottom shoulder
)

;;; Centreline through a feature, running past both ends.
(defun MachFeat:CentreLine ( base ang overall )
    (MachFeat:Centre)
    (MachFeat:Line
        (list (MachFeat:Pt base ang (- MachFeat:CLEXTEND) 0.0)
              (MachFeat:Pt base ang (+ overall MachFeat:CLEXTEND) 0.0)))
)

;;; ---------------------------------------------------------------------------
;;; THE FEATURES
;;;
;;; Each takes the point where the feature breaks the surface, on the axis, and
;;; the direction the feature runs into the material.
;;; ---------------------------------------------------------------------------

;;; A plain drilled hole, open at the surface.
(defun MachFeat:Drilled ( base ang dia depth / rad pt )
    (setq rad (/ dia 2.0)
          pt  (MachFeat:DrillPoint base ang rad depth))
    (MachFeat:Object)
    ;; One open run: down one side, round the point, back up the other.
    (MachFeat:Line
        (append (list (MachFeat:Pt base ang 0.0 rad)) pt
                (list (MachFeat:Pt base ang 0.0 (- rad)))))
    ;; The line across at full depth, which is the dimension that gets called up.
    (MachFeat:Line (list (car pt) (caddr pt)))
    (MachFeat:CentreLine base ang (+ depth (/ rad MachFeat:COS30 2.0)))
    (princ)
)

;;; A drilled and tapped hole. The tapping drill is drawn as a normal hole and
;;; the thread envelope laid over it in hidden line.
(defun MachFeat:Tapped ( base ang dia depth / rad drad tdepth )
    (setq rad    (/ dia 2.0)
          drad   (/ (* dia MachFeat:TAPDRILL) 2.0)
          ;; Threads stop short of the drilled depth - the tap has a lead and
          ;; cannot cut a full thread to the bottom of a blind hole.
          tdepth (- depth MachFeat:CLEXTEND))
    (if (<= tdepth 0.0) (setq tdepth (* depth 0.5)))

    (MachFeat:Drilled base ang (* dia MachFeat:TAPDRILL) depth)

    (MachFeat:Hidden)
    (MachFeat:Line
        (list (MachFeat:Pt base ang 0.0    rad)
              (MachFeat:Pt base ang tdepth rad)
              (MachFeat:Pt base ang tdepth (- rad))
              (MachFeat:Pt base ang 0.0    (- rad))))
    (princ)
)

;;; A tapped hole seen from above: the tapping drill solid, the thread outside
;;; diameter hidden.
(defun MachFeat:Plan ( base dia )
    (MachFeat:Object)
    (command "_.CIRCLE" base (/ (* dia MachFeat:TAPDRILL) 2.0))
    (MachFeat:Hidden)
    (command "_.CIRCLE" base (/ dia 2.0))
    (princ)
)

;;; A drilled hole with a flat-bottomed recess above it.
(defun MachFeat:Counterbore ( base ang dia depth cbdia cbdepth
                              / rad cbrad below floor pt )
    (setq rad    (/ dia 2.0)
          cbrad  (/ cbdia 2.0)
          ;; The stated drill depth is from the SURFACE, so the drilling still
          ;; to do below the counterbore floor is the difference.
          below  (- depth cbdepth)
          floor  (MachFeat:Pt base ang cbdepth 0.0))

    (if (<= below 0.0)
        (princ "\n** The drill is not deeper than the counterbore - nothing drawn. **")
        (progn
            (setq pt (MachFeat:DrillPoint floor ang rad below))
            (MachFeat:Object)
            ;; One run from the surface at counterbore diameter, in to the drill,
            ;; round the point and back out.
            (MachFeat:Line
                (append (list (MachFeat:Pt base ang 0.0     cbrad)
                              (MachFeat:Pt base ang cbdepth cbrad)
                              (MachFeat:Pt base ang cbdepth rad))
                        pt
                        (list (MachFeat:Pt base ang cbdepth (- rad))
                              (MachFeat:Pt base ang cbdepth (- cbrad))
                              (MachFeat:Pt base ang 0.0     (- cbrad)))))
            ;; The counterbore floor, and the line at full drill depth.
            (MachFeat:Line (list (MachFeat:Pt base ang cbdepth rad)
                                 (MachFeat:Pt base ang cbdepth (- rad))))
            (MachFeat:Line (list (car pt) (caddr pt)))
            (MachFeat:CentreLine base ang (+ depth (/ rad MachFeat:COS30 2.0)))))
    (princ)
)

;;; A drilled hole with a conical recess above it.
(defun MachFeat:Countersink ( base ang dia depth csdia csdepth
                              / rad csrad slant below pt )
    (setq rad   (/ dia 2.0)
          csrad (/ csdia 2.0)
          ;; Slant length of the countersink cone, from its rim down to where it
          ;; meets the drill.
          slant (/ (- csrad rad) MachFeat:COS30)
          below (- depth csdepth))

    (if (or (<= below 0.0) (<= csrad rad))
        (princ "\n** The countersink must be wider than the drill and shallower than it. **")
        (progn
            (setq pt (MachFeat:DrillPoint
                         (MachFeat:Pt base ang (+ csdepth (* slant 0.5)) 0.0)
                         ang rad (- below (* slant 0.5))))
            (MachFeat:Object)
            (MachFeat:Line
                (append (list (MachFeat:Pt base ang 0.0 csrad)
                              (MachFeat:Pt base ang csdepth csrad)
                              (MachFeat:Pt base ang (+ csdepth (* slant 0.5)) rad))
                        pt
                        (list (MachFeat:Pt base ang (+ csdepth (* slant 0.5)) (- rad))
                              (MachFeat:Pt base ang csdepth (- csrad))
                              (MachFeat:Pt base ang 0.0 (- csrad)))))
            ;; Rim of the countersink, and where the cone meets the drill.
            (MachFeat:Line (list (MachFeat:Pt base ang csdepth csrad)
                                 (MachFeat:Pt base ang csdepth (- csrad))))
            (MachFeat:Line (list (MachFeat:Pt base ang (+ csdepth (* slant 0.5)) rad)
                                 (MachFeat:Pt base ang (+ csdepth (* slant 0.5)) (- rad))))
            (MachFeat:Line (list (car pt) (caddr pt)))
            (MachFeat:CentreLine base ang (+ depth (/ rad MachFeat:COS30 2.0)))))
    (princ)
)

;;; A dowel pin: a cylinder with a chamfer at each end.
(defun MachFeat:Dowel ( base ang dia length / rad cham body slant flat )
    (setq rad  (/ dia 2.0)
          ;; Standard dowel chamfers step at 5/16 diameter.
          cham (if (>= dia 0.3125) 0.03 0.015)
          body (- length (* cham 2.0)))

    (if (<= body 0.0)
        (princ "\n** The pin is too short for its end chamfers. **")
        (progn
            (setq flat (- rad cham))     ; radius at the end face, after chamfer
            (MachFeat:Object)
            ;; Outline: end face, chamfer out, along the body, chamfer in, end
            ;; face, and back along the other side.
            (MachFeat:Line
                (list (MachFeat:Pt base ang 0.0            flat)
                      (MachFeat:Pt base ang cham           rad)
                      (MachFeat:Pt base ang (+ cham body)  rad)
                      (MachFeat:Pt base ang length         flat)
                      (MachFeat:Pt base ang length         (- flat))
                      (MachFeat:Pt base ang (+ cham body)  (- rad))
                      (MachFeat:Pt base ang cham           (- rad))
                      (MachFeat:Pt base ang 0.0            (- flat))
                      (MachFeat:Pt base ang 0.0            flat)))
            ;; The two chamfer lines, which is what makes it read as a dowel
            ;; rather than as a plain rectangle.
            (MachFeat:Line (list (MachFeat:Pt base ang cham rad)
                                 (MachFeat:Pt base ang cham (- rad))))
            (MachFeat:Line (list (MachFeat:Pt base ang (+ cham body) rad)
                                 (MachFeat:Pt base ang (+ cham body) (- rad))))))
    (princ)
)

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

(defun c:MACHFEAT ( / *error* vars vals kind base ang dia depth cbdia cbdepth
                      length v )

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

    (defun MachFeat: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 )
        (MachFeat:Restore)
        (if (and msg (not (wcmatch (strcase msg t) "*break*,*cancel*,*exit*")))
            (princ (strcat "\n** MACHFEAT 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 "Drilled Tapped Counterbore Countersink Plan Dowel")
    (setq kind (getkword
                   (strcat "\nFeature [Drilled/Tapped/Counterbore/Countersink/Plan/Dowel] <"
                           (MachFeat:Get "TYPE") ">: ")))
    (if (null kind) (setq kind (MachFeat:Get "TYPE")))
    (MachFeat:Put "TYPE" kind)

    ;; Diameter is wanted by every feature.
    (initget 6)
    (setq v (getdist (strcat "\n"
                             (cond ((= kind "Tapped") "Thread outside diameter")
                                   ((= kind "Plan")   "Thread outside diameter")
                                   ((= kind "Dowel")  "Dowel pin diameter")
                                   (t                 "Drill diameter"))
                             " <" (rtos (MachFeat:Get "DIA") 2 4) ">: ")))
    (setq dia (if v (MachFeat:Put "DIA" v) (MachFeat:Get "DIA")))

    ;; Depth or length, depending on the feature. The plan view needs neither.
    (cond
        ((= kind "Plan") nil)

        ((= kind "Dowel")
         (initget 6)
         (setq v (getdist (strcat "\nDowel pin length <"
                                  (rtos (MachFeat:Get "LENGTH") 2 4) ">: ")))
         (setq length (if v (MachFeat:Put "LENGTH" v) (MachFeat:Get "LENGTH"))))

        (t
         (initget 6)
         (setq v (getdist (strcat "\nDrill depth to full diameter <"
                                  (rtos (MachFeat:Get "DEPTH") 2 4) ">: ")))
         (setq depth (if v (MachFeat:Put "DEPTH" v) (MachFeat:Get "DEPTH")))))

    ;; The two recessed features need their recess described as well.
    (if (member kind '("Counterbore" "Countersink"))
        (progn
            (initget 6)
            (setq v (getdist (strcat "\n" kind " diameter <"
                                     (rtos (MachFeat:Get "CBDIA") 2 4) ">: ")))
            (setq cbdia (if v (MachFeat:Put "CBDIA" v) (MachFeat:Get "CBDIA")))
            (initget 6)
            (setq v (getdist (strcat "\n" kind " depth <"
                                     (rtos (MachFeat:Get "CBDEPTH") 2 4) ">: ")))
            (setq cbdepth (if v (MachFeat:Put "CBDEPTH" v) (MachFeat:Get "CBDEPTH")))))

    ;; Direction, except for the plan view which has none.
    (if (/= kind "Plan")
        (progn
            (setq v (getangle (strcat "\nDirection the feature runs <"
                                      (angtos (MachFeat:Get "ANG") 0 2) ">: ")))
            (setq ang (if v (MachFeat:Put "ANG" v) (MachFeat:Get "ANG"))))
        (setq ang 0.0))

    (setvar "BLIPMODE" 0)

    ;; Place as many as wanted - a plate full of identical holes is the norm.
    (while (setq base (getpoint
                          (strcat "\n"
                                  (if (member kind '("Plan" "Dowel"))
                                      "Centre" "Where the feature breaks the surface")
                                  " <Enter to finish>: ")))
        (setvar "OSMODE" 0)
        (cond
            ((= kind "Drilled")     (MachFeat:Drilled     base ang dia depth))
            ((= kind "Tapped")      (MachFeat:Tapped      base ang dia depth))
            ((= kind "Plan")        (MachFeat:Plan        base dia))
            ((= kind "Counterbore") (MachFeat:Counterbore base ang dia depth cbdia cbdepth))
            ((= kind "Countersink") (MachFeat:Countersink base ang dia depth cbdia cbdepth))
            ((= kind "Dowel")       (MachFeat:Dowel       base ang dia length))
        )
        (setvar "OSMODE" (nth (vl-position "OSMODE" vars) vals))
    )

    (MachFeat:Restore)
    (princ)
)

;;; ---------------------------------------------------------------------------
;;; SURFACE ROUGHNESS SYMBOL
;;;
;;; The tick-and-tail mark that carries a maximum surface roughness value. Two
;;; legs meeting at the surface, the longer one carrying the number.
;;;
;;; AutoCAD has never had a native equivalent, so this stays worth having.
;;; ---------------------------------------------------------------------------

(defun c:FINISHMARK ( / *error* vars vals base ang value size a b c height )

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

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

    ;; The symbol is sized from DIMSCALE so it matches the drawing's dimensions
    ;; rather than being fixed at the imperial sizes the original hard-coded.
    (setq size (getvar "DIMSCALE"))
    (if (zerop size) (setq size 1.0))
    (setq height (* size (getvar "DIMTXT")))
    (if (<= height 0.0) (setq height (* size 0.07)))

    (setq value (getstring t "\nMaximum surface roughness: "))
    (setvar "BLIPMODE" 0)

    (while (setq base (getpoint "\nPoint on the surface <Enter to finish>: "))
        (setq ang (getangle base "\nRotation <0>: "))
        (if (null ang) (setq ang 0.0))
        (setvar "OSMODE" 0)

        ;; Two legs at 60 degrees to the surface, the right-hand one three times
        ;; the length of the left, which is the standard proportion.
        (setq a (polar base (+ ang (/ pi 3.0) (/ pi 3.0)) (* size 0.0808))
              b (polar base (+ ang (/ pi 3.0))            (* size 0.2425))
              c (polar a   (+ ang (/ pi 2.0))             (* size 0.035)))

        (MachFeat:Layer "MACHFEAT-OBJECT" nil)
        (MachFeat:Line (list a base b))

        (if (/= value "")
            (command "_.TEXT" "_J" "_BL" c height (/ (* ang 180.0) pi) value))

        (setvar "OSMODE" (nth (vl-position "OSMODE" vars) vals))
    )

    (MachFeat:Restore)
    (princ)
)

(princ)
