;;; ---------------------------------------------------------------------------
;;; CamProfile.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; PLATE CAM PROFILE FROM A MOTION LAW
;;;
;;; PURPOSE
;;;   Draws the working face of a plate cam - the shape that has to be cut so a
;;;   roller follower rises by a given amount over a given angle, in a given
;;;   way.
;;;
;;; THE THREE MOTIONS
;;;   Constant velocity  The follower rises at a steady rate. Simple, and
;;;                      violent: the acceleration is infinite at each end, so
;;;                      it hammers. Use only at low speed.
;;;   Harmonic           Follows a cosine. Smooth in the middle, but the
;;;                      acceleration jumps at the ends, which rings.
;;;   Cycloidal          Acceleration starts and finishes at zero, so nothing
;;;                      jerks. The one to use at speed, and the default.
;;;
;;;   Written as a fraction f of the way through the rise, with h the lift:
;;;
;;;     constant   s = h f
;;;     harmonic   s = h (1 - cos(pi f)) / 2
;;;     cycloidal  s = h (f - sin(2 pi f) / (2 pi))
;;;
;;; PITCH CURVE AND CAM FACE
;;;   The follower's CENTRE traces the pitch curve - base radius plus follower
;;;   radius plus the lift. The cam FACE is that curve pulled in by the follower
;;;   radius, along the normal at each point. Cutting to the pitch curve rather
;;;   than the face is the classic mistake, so both are drawn, on separate
;;;   layers, and which is which is said plainly.
;;;
;;; WHAT WAS FIXED
;;;   - The cam face was drawn by starting a LINE command and feeding it the
;;;     word "TAN" followed by a coordinate, over and over. TAN is an object
;;;     snap: it needs an object picked near a point, not a point. Nothing
;;;     usable came out.
;;;   - Both motion laws were written as decimal constants - 0.43990, 0.035010,
;;;     0.280050, 0.315050 - with no indication of where they came from. They
;;;     are a cycloidal rise scaled to an odd fraction of the period, and the
;;;     second one is the first with an offset bolted on. Both are written as
;;;     the standard forms above.
;;;   - Fifteen variables were global.
;;;   - It finished by printing a company's telephone number to the command line.
;;;
;;;   CAM  - draw a plate cam profile
;;; ---------------------------------------------------------------------------

(setq *Cam:Base* nil *Cam:Roller* nil *Cam:Lift* nil)

(defun Cam: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 Cam:Poly ( pts layer closed )
    (entmake (append
        (list '(0 . "LWPOLYLINE") '(100 . "AcDbEntity") (cons 8 layer)
              '(100 . "AcDbPolyline") (cons 90 (length pts))
              (cons 70 (if closed 1 0)))
        (mapcar '(lambda ( p ) (cons 10 (list (car p) (cadr p)))) pts)))
)

;;; Lift at fraction F of the way through a rise of H.
(defun Cam:Lift ( f h law )
    (cond ((= law "Constant")  (* h f))
          ((= law "Harmonic")  (* h (/ (- 1.0 (cos (* pi f))) 2.0)))
          (t (* h (- f (/ (sin (* 2.0 pi f)) (* 2.0 pi))))))
)

(defun c:CAM ( / *error* vars vals cen base roller lift period offset law
                 steps i f ang r pitch face lay1 lay2 v n
                 p prev nxt norm )

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

    (defun Cam: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 )
        (Cam:Restore)
        (if (and msg (not (wcmatch (strcase msg t) "*break*,*cancel*,*exit*")))
            (princ (strcat "\n** CAM 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 cen (getpoint "\nCentre of the cam: "))

    (if (null cen)
        (princ "\nCancelled.")
        (progn
            (setvar "OSMODE" 0)
            (setq cen (list (car cen) (cadr cen) 0.0))

            (initget 6)
            (setq base (getdist cen (strcat "\nBase circle radius"
                                            (if *Cam:Base*
                                                (strcat " <" (rtos *Cam:Base* 2 3) ">") "")
                                            ": ")))
            (if (null base) (setq base *Cam:Base*))
            (initget 6)
            (setq roller (getdist (strcat "\nRoller follower radius"
                                          (if *Cam:Roller*
                                              (strcat " <" (rtos *Cam:Roller* 2 3) ">") "")
                                          ": ")))
            (if (null roller) (setq roller *Cam:Roller*))
            (initget 6)
            (setq lift (getdist (strcat "\nTotal lift"
                                        (if *Cam:Lift*
                                            (strcat " <" (rtos *Cam:Lift* 2 3) ">") "")
                                        ": ")))
            (if (null lift) (setq lift *Cam:Lift*))

            (if (not (and base roller lift))
                (princ "\nNot enough given to draw a cam.")
                (progn
                    (setq *Cam:Base* base *Cam:Roller* roller *Cam:Lift* lift)

                    (initget 6)
                    (setq period (getreal "\nRise over how many degrees <180>: "))
                    (if (null period) (setq period 180.0))
                    (if (> period 360.0) (setq period 360.0))

                    (initget 4)
                    (setq offset (getreal "\nRise starts at what angle <0>: "))
                    (if (null offset) (setq offset 0.0))

                    (initget "Cycloidal Harmonic Constant")
                    (setq law (getkword "\nMotion [Cycloidal/Harmonic/Constant] <Cycloidal>: "))
                    (if (null law) (setq law "Cycloidal"))

                    (initget 6)
                    (setq steps (getint "\nPoints per degree <2>: "))
                    (if (null steps) (setq steps 2))

                    ;; --- the pitch curve, all the way round ------------------
                    ;; Outside the rise the follower sits on the base circle;
                    ;; inside it, it climbs by the motion law.
                    (setq pitch nil i 0 n (* 360 steps))
                    (while (<= i n)
                        (setq ang (/ (* 2.0 pi i) n)
                              f   (/ (- (/ (* 180.0 ang) pi) offset) period)
                              r   (+ base roller
                                     (cond ((< f 0.0) 0.0)
                                           ((> f 1.0) lift)
                                           (t (Cam:Lift f lift law))))
                              pitch (cons (polar cen ang r) pitch)
                              i (1+ i)))
                    (setq pitch (reverse pitch))

                    ;; --- the cam face ---------------------------------------
                    ;; Each face point is its pitch point pulled in by the
                    ;; roller radius, along the normal to the pitch curve at
                    ;; that point - NOT straight toward the centre, which is
                    ;; only the same thing where the curve is flat.
                    (setq face nil i 0 v (length pitch))
                    (while (< i v)
                        (setq p    (nth i pitch)
                              prev (nth (rem (+ (1- i) v) v) pitch)
                              nxt  (nth (rem (1+ i) v) pitch)
                              ;; Normal is the tangent turned a quarter turn.
                              norm (+ (angle prev nxt) (/ pi 2.0))
                              face (cons (polar p norm roller) face)
                              i (1+ i)))
                    (setq face (reverse face))

                    (setq lay1 (Cam:Layer "Cam-Face" 7)
                          lay2 (Cam:Layer "Cam-Pitch" 8))

                    (Cam:Poly face lay1 t)
                    (Cam:Poly pitch lay2 t)

                    ;; Base circle and centre, for setting out.
                    (entmake (list '(0 . "CIRCLE") (cons 8 lay2)
                                   (cons 10 cen) (cons 40 base)))
                    (entmake (list '(0 . "POINT") (cons 8 lay2) (cons 10 cen)))

                    (princ (strcat "\n" law " rise of " (rtos lift 2 3)
                                   " over " (rtos period 2 1) " degrees."
                                   "\n  Cut to the CAM-FACE outline."
                                   "\n  CAM-PITCH is the follower centre path -"
                                   " do not cut to it."
                                   "\n  Least radius on the face "
                                   (rtos base 2 3) ", greatest "
                                   (rtos (+ base lift) 2 3) ".")))))
    )

    (Cam:Restore)
    (princ)
)

(princ)
