;;; ---------------------------------------------------------------------------
;;; ClockFace.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; A LONGCASE CLOCK, DRAWN OR RUNNING
;;;
;;; PURPOSE
;;;   Draws a longcase (grandfather) clock - case, hood, dial, hour marks and
;;;   pendulum - with the hands set to the current time or to any time you give.
;;;
;;;   Two modes:
;;;
;;;     Draw   real geometry you can keep, plot and edit. A clock face is a
;;;            genuine thing to put on a drawing - schools, stations, foyers.
;;;
;;;     Live   the original novelty: the clock runs on screen with a swinging
;;;            pendulum, drawn as temporary vectors, until you press Escape.
;;;            Nothing is added to the drawing.
;;;
;;; WHERE THE SHAPE COMES FROM
;;;   The case outline is a vector list carried over unchanged from 1986 - a
;;;   sequence of triples, each a pen code and a coordinate, in a 1200-unit tall
;;;   space. Code 3 lifts the pen and moves; code 4 draws to the point.
;;;
;;;   That is a plotter instruction stream, and it is preserved exactly. It is
;;;   somebody's drawing of a clock, and there is nothing to derive it from.
;;;   What has changed is what happens to it: the runs between pen-ups are now
;;;   collected into polylines and drawn as real objects, instead of being
;;;   flicked onto the screen as vectors that vanish at the next regen.
;;;
;;; WHY THE ORIGINAL COULD NOT BE KEPT AS IT WAS
;;;   It ran in an unbounded loop with no exit but Escape, wrote to the screen
;;;   menu area through GRTEXT - which has not existed for many releases - and
;;;   produced no geometry at all. Run it and you had a picture until the next
;;;   redraw, and nothing to show for it.
;;;
;;;   CLOCKFACE  - draw a longcase clock, or run it live
;;; ---------------------------------------------------------------------------

;;; ---------------------------------------------------------------------------
;;; THE CASE OUTLINE
;;;
;;; Pen code, X, Y - repeating. 3 = move, 4 = draw. Coordinates are in the
;;; original 1200-unit space; everything is scaled from that on the way out.
;;; ---------------------------------------------------------------------------

(setq ClockFace:Art
    '(3 791 893  4 789 913  4 783 933  4 773 951  4 760 966  4 745 979
      4 727 989  4 708 994  4 688 996  4 667 994  4 648 989  4 630 979
      4 615 966  4 602 951  4 592 933  4 586 913  4 584 893  4 586 873
      4 592 854  4 602 836  4 615 820  4 630 808  4 648 798  4 667 792
      4 688 790  4 708 792  4 727 798  4 745 808  4 760 820  4 773 836
      4 783 854  4 789 873  4 791 893  4 771 893
      3 760 935  4 777 945   3 729 966  4 739 983   3 688 977  4 688 996
      3 636 983  4 646 966   3 615 935  4 598 945   3 604 893  4 584 893
      3 598 842  4 615 851   3 646 821  4 636 804   3 688 809  4 688 790
      3 739 804  4 729 821   3 760 851  4 777 842
      3 791 756  4 791 241  4 584 241  4 550 206  4 550 137  4 825 137
      4 825 206  4 791 241
      3 825 206  4 550 206
      3 584 241  4 584 756
      3 550 756  4 550 1031  4 825 1031  4 825 756  4 550 756
      3 516 996  4 688 1168  4 859 996
      3 773 1048 4 602 1048  4 688 1134  4 773 1048))

;;; Reference points in that same 1200-unit space.
(setq ClockFace:DIALX  688.0    ; centre of the dial
      ClockFace:DIALY  893.0
      ClockFace:PIVX   688.0    ; pendulum pivot
      ClockFace:PIVY   722.0
      ClockFace:PENDL  413.0    ; pendulum rod length
      ClockFace:MINL    90.0    ; minute hand
      ClockFace:HOURL   60.0    ; hour hand
      ClockFace:BASEY  137.0    ; underside of the case
      ClockFace:FULLH 1200.0)   ; the space the artwork was drawn in

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

(if (null *ClockFace:Prefs*)
    (setq *ClockFace:Prefs* (list (cons "HEIGHT" 60.0))))

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

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

;;; ---------------------------------------------------------------------------
;;; PLACEMENT
;;;
;;; The picked point is the centre of the base, which is where a clock stands.
;;; ---------------------------------------------------------------------------

(defun ClockFace:P ( org scale x y )
    (list (+ (car  org) (* scale (- x ClockFace:DIALX)))
          (+ (cadr org) (* scale (- y ClockFace:BASEY)))
          0.0)
)

;;; Split the pen stream into runs, each a list of points between pen-ups.
(defun ClockFace:Runs ( org scale / lst code x y run out )
    (setq lst ClockFace:Art out nil run nil)
    (while lst
        (setq code (car lst) x (float (cadr lst)) y (float (caddr lst))
              lst  (cdddr lst))
        (if (= code 3)
            (progn
                ;; Pen up: bank whatever was being drawn and start again.
                (if (> (length run) 1) (setq out (cons (reverse run) out)))
                (setq run (list (ClockFace:P org scale x y))))
            (setq run (cons (ClockFace:P org scale x y) run))))
    (if (> (length run) 1) (setq out (cons (reverse run) out)))
    (reverse out)
)

;;; ---------------------------------------------------------------------------
;;; THE TIME
;;;
;;; Returns ( hour-angle minute-angle hh mm ) with the angles in radians,
;;; measured the way a clock reads: twelve at the top, running clockwise.
;;; ---------------------------------------------------------------------------

(defun ClockFace:Hands ( secs / hh mm )
    (setq hh (fix (/ secs 3600.0))
          mm (fix (/ (- secs (* hh 3600.0)) 60.0)))
    (list
        ;; The hour hand creeps: it is driven by the whole elapsed time, not by
        ;; the hour alone, so at half past it sits midway between the numerals.
        (- (* pi 0.5) (* 2.0 pi (/ secs 43200.0)))
        (- (* pi 0.5) (* 2.0 pi (/ (- secs (* hh 3600.0)) 3600.0)))
        hh mm)
)

;;; Seconds since midnight, from the Julian date AutoCAD keeps in DATE.
(defun ClockFace:Now ( / d ) (setq d (getvar "DATE")) (* 86400.0 (- d (fix d))))

(defun ClockFace:Clock12 ( hh mm )
    (strcat (itoa (if (zerop (rem hh 12)) 12 (rem hh 12))) ":"
            (if (< mm 10) "0" "") (itoa mm)
            (if (< hh 12) " AM" " PM"))
)

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

(defun c:CLOCKFACE ( / *error* vars vals mode org height scale secs v
                       hands dial piv runs input swing r pend tp np )

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

    (defun ClockFace: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 )
        (redraw)
        (ClockFace:Restore)
        (if (and msg (not (wcmatch (strcase msg t) "*break*,*cancel*,*exit*")))
            (princ (strcat "\n** CLOCKFACE 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.
    (vl-catch-all-apply '(lambda ( ) (*push-error-using-command*)) '())
    (command "_.UNDO" "_Begin")

    (initget "Draw Live")
    (setq mode (getkword "\n[Draw as geometry/Live on screen] <Draw>: "))
    (if (null mode) (setq mode "Draw"))

    (setq org (getpoint "\nCentre of the base of the clock: "))

    (if (null org)
        (princ "\nCancelled.")
        (progn
            (initget 6)
            (setq v (getdist org (strcat "\nOverall height <"
                                         (rtos (ClockFace:Get "HEIGHT") 2 3) ">: ")))
            (if v (ClockFace:Put "HEIGHT" v))
            (setq height (ClockFace:Get "HEIGHT")
                  scale  (/ height ClockFace:FULLH))

            ;; Time to show. A blank answer takes the clock on the wall.
            (initget 4)
            (setq v (getreal "\nHour to show, 0 to 24, or Enter for now: "))
            (setq secs (if v (* v 3600.0) (ClockFace:Now)))
            (setq hands (ClockFace:Hands secs)
                  dial  (ClockFace:P org scale ClockFace:DIALX ClockFace:DIALY)
                  piv   (ClockFace:P org scale ClockFace:PIVX  ClockFace:PIVY)
                  runs  (ClockFace:Runs org scale))

            (setvar "OSMODE" 0)
            (setvar "BLIPMODE" 0)

            (if (= mode "Draw")
                ;; ---------------- real geometry ---------------------------
                (progn
                    (if (tblsearch "layer" "CLOCK")
                        (command "_.LAYER" "_ON" "CLOCK" "_THAW" "CLOCK" "_UNLOCK" "CLOCK" "")
                        (command "_.LAYER" "_NEW" "CLOCK" ""))
                    (setvar "CLAYER" "CLOCK")

                    (foreach run runs
                        (command "_.PLINE" (car run) "_W" 0 0)
                        (foreach p (cdr run) (command p))
                        (command ""))

                    ;; Hands, at the time asked for.
                    (command "_.LINE" dial
                             (polar dial (car hands) (* scale ClockFace:HOURL)) "")
                    (command "_.LINE" dial
                             (polar dial (cadr hands) (* scale ClockFace:MINL)) "")

                    ;; Pendulum at rest, hanging straight down from its pivot.
                    (command "_.LINE" piv
                             (polar piv (* pi 1.5) (* scale ClockFace:PENDL)) "")
                    (command "_.CIRCLE"
                             (polar piv (* pi 1.5) (* scale ClockFace:PENDL))
                             (* scale 30.0))

                    (princ (strcat "\nClock drawn, showing "
                                   (ClockFace:Clock12 (caddr hands) (cadddr hands)) ".")))

                ;; ---------------- live on screen --------------------------
                ;; Temporary vectors only - nothing is added to the drawing.
                ;; The original looped forever with no exit but Escape; this
                ;; watches for any input and stops cleanly.
                (progn
                    (princ "\nRunning. Press Escape or click to stop.")

                    ;; The pendulum swings nine degrees either side, which is
                    ;; where the sine in the original's angle comes from.
                    (setq r 0.0 pend nil)
                    (while (<= r (* 2.0 pi))
                        (setq pend (cons (polar piv
                                                (+ (* pi 1.5)
                                                   (* (/ pi 20.0) (sin r)))
                                                (* scale ClockFace:PENDL))
                                         pend)
                              r (+ r 0.25)))
                    (setq pend (reverse pend) tp (car pend))

                    (foreach run runs
                        (setq np (car run))
                        (foreach p (cdr run) (grdraw np p 7) (setq np p)))

                    (setq input nil)
                    (while (null input)
                        (setq secs  (ClockFace:Now)
                              hands (ClockFace:Hands secs))
                        (grdraw dial (polar dial (car hands) (* scale ClockFace:HOURL)) -1)
                        (grdraw dial (polar dial (cadr hands) (* scale ClockFace:MINL)) -1)

                        (foreach np pend
                            (grdraw piv np -1)
                            (grdraw piv tp -1)
                            (setq tp np))

                        ;; GRREAD with no wait returns nil when nothing has been
                        ;; typed or clicked, so the loop keeps running until it
                        ;; does - and then stops instead of needing Escape.
                        (setq input (grread nil 4 0))
                        (if (and input (= 5 (car input))) (setq input nil)))

                    (redraw)
                    (princ "\nStopped."))
            )
        )
    )

    (ClockFace:Restore)
    (princ)
)

(princ)
