;;; ---------------------------------------------------------------------------
;;; SurfacePlot.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; PLOT A SURFACE FROM AN EQUATION
;;;
;;; PURPOSE
;;;   Draws z = f(x,y) as a 3D mesh over a range you give. Saddles, domes,
;;;   ripples, decay curves, anything that can be written as an expression.
;;;
;;;   The equation is typed in LISP form, which is prefix notation - the
;;;   operator first:
;;;
;;;       (* 0.2 (+ (* x x) (* y y)))          a paraboloid
;;;       (* 5 (sin (/ (sqrt (+ (* x x) (* y y))) 3)))   ripples
;;;       (/ (* x y) 10)                       a saddle
;;;       (* 10 (exp (- (* 0.05 (+ (* x x) (* y y))))))  a bell
;;;
;;;   Anything AutoLISP can evaluate will do: sin cos atan sqrt expt exp log abs.
;;;
;;; WHAT WAS FIXED
;;;   The original did not plot anything. It WROTE A LISP FILE to disk,
;;;   line by line, containing a program that would do the plotting - then wrote
;;;   a second file, a script, whose job was to load the first one and run it.
;;;   Then it ran the script.
;;;
;;;   Sixty lines of the source are string concatenation assembling that program,
;;;   with (chr 34) standing in for every quotation mark. Any mistake in the
;;;   equation surfaced as a syntax error in a generated file the user never saw.
;;;   It also left both files behind, and the generated one took the name the
;;;   user gave with no check that it was not something already on disk.
;;;
;;;   AutoLISP can evaluate an expression directly - (eval (read string)) - so
;;;   none of that machinery is needed. The equation is checked once before the
;;;   mesh is built, and a mistake is reported plainly.
;;;
;;;   SURFPLOT  - plot z = f(x,y) as a 3D mesh
;;; ---------------------------------------------------------------------------

(setq *Surf:Expr* nil)

;;; Evaluate the expression with X and Y bound. Returns nil if it will not
;;; evaluate, which is how a bad equation is caught before any mesh is built.
;;; X and Y are deliberately global for the duration - that is how the typed
;;; expression can refer to them by name.
(defun Surf:Eval ( expr xv yv / r )
    (setq x xv y yv
          r (vl-catch-all-apply 'eval (list expr)))
    (if (or (vl-catch-all-error-p r) (not (numberp r))) nil (float r))
)

(defun c:SURFPLOT ( / *error* vars vals txt expr x1 x2 y1 y2 nx ny lay
                      org i j xv yv z pts bad v step1 step2 lo hi )

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

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

    (princ "\nType the equation for z, in LISP form, using x and y.")
    (princ "\n  For example:  (* 0.2 (+ (* x x) (* y y)))")
    (setq txt (getstring t (strcat "\nz = "
                                   (if *Surf:Expr*
                                       (strcat "<" *Surf:Expr* "> ") ""))))
    (if (and (= "" txt) *Surf:Expr*) (setq txt *Surf:Expr*))

    (if (= "" txt)
        (princ "\nNo equation given.")
        (progn
            (setq expr (vl-catch-all-apply 'read (list txt)))

            (if (vl-catch-all-error-p expr)
                (princ "\n** That will not read as a LISP expression - check the brackets. **")
                (progn
                    ;; Try it once before building anything.
                    (if (null (Surf:Eval expr 1.0 1.0))
                        (princ (strcat "\n** That does not evaluate to a number."
                                       "\n   Remember the operator comes first:"
                                       " (* 2 x), not (x * 2). **"))
                        (progn
                            (setq *Surf:Expr* txt)

                            (initget 1)
                            (setq x1 (getreal "\nLowest x: "))
                            (initget 1)
                            (setq x2 (getreal "\nHighest x: "))
                            (initget 1)
                            (setq y1 (getreal "\nLowest y: "))
                            (initget 1)
                            (setq y2 (getreal "\nHighest y: "))

                            (initget 6)
                            (setq nx (getint "\nSteps across x <20>: "))
                            (if (null nx) (setq nx 20))
                            (initget 6)
                            (setq ny (getint "\nSteps across y <20>: "))
                            (if (null ny) (setq ny 20))

                            (if (or (> nx 254) (> ny 254))
                                (progn (princ "\n  A mesh is limited to 254 either way.")
                                       (setq nx (min nx 254) ny (min ny 254))))

                            (setq org (getpoint "\nWhere to put the origin: "))
                            (if (null org) (setq org '(0.0 0.0 0.0)))

                            (setvar "OSMODE" 0)
                            (setq lay (if (tblsearch "LAYER" "Surface")
                                          "Surface"
                                          (progn
                                              (entmake (list '(0 . "LAYER")
                                                             '(100 . "AcDbSymbolTableRecord")
                                                             '(100 . "AcDbLayerTableRecord")
                                                             '(2 . "Surface") '(70 . 0)
                                                             '(62 . 4) '(6 . "Continuous")))
                                              "Surface")))

                            ;; --- work out every point first -----------------
                            ;; If any of them fails to evaluate, nothing is
                            ;; drawn - a half-built mesh is worse than none.
                            (setq step1 (/ (- x2 x1) (float nx))
                                  step2 (/ (- y2 y1) (float ny))
                                  pts nil bad 0 i 0 lo nil hi nil)

                            (while (<= i nx)
                                (setq xv (+ x1 (* i step1)) j 0)
                                (while (<= j ny)
                                    (setq yv (+ y1 (* j step2))
                                          z  (Surf:Eval expr xv yv))
                                    (if (null z)
                                        (setq bad (1+ bad) z 0.0))
                                    (if (null lo) (setq lo z hi z)
                                        (setq lo (min lo z) hi (max hi z)))
                                    (setq pts (cons (list (+ (car org) xv)
                                                          (+ (cadr org) yv)
                                                          (+ (caddr org) z))
                                                    pts)
                                          j (1+ j)))
                                (setq i (1+ i)))
                            (setq pts (reverse pts))

                            ;; --- build the mesh -----------------------------
                            ;; A polygon mesh is a header giving its size, then
                            ;; one vertex per point in row order, then a SEQEND.
                            (entmake (list '(0 . "POLYLINE") (cons 8 lay)
                                           '(66 . 1) '(70 . 16)
                                           (cons 71 (1+ nx)) (cons 72 (1+ ny))
                                           '(10 0.0 0.0 0.0)))
                            (foreach p pts
                                (entmake (list '(0 . "VERTEX") (cons 8 lay)
                                               (cons 10 p) '(70 . 64))))
                            (entmake (list '(0 . "SEQEND") (cons 8 lay)))

                            (princ (strcat "\nMesh " (itoa (1+ nx)) " by "
                                           (itoa (1+ ny)) " over x "
                                           (rtos x1 2 3) " to " (rtos x2 2 3)
                                           ", y " (rtos y1 2 3) " to " (rtos y2 2 3)
                                           "\n  z ranges " (rtos lo 2 4) " to "
                                           (rtos hi 2 4) "."))
                            (if (> bad 0)
                                (princ (strcat "\n  " (itoa bad)
                                               " points would not evaluate -"
                                               " set flat. Division by zero,"
                                               " or a root of a negative.")))
                            (princ "\n  VPOINT or 3DORBIT to look at it, HIDE to shade.")))))))

    ;; X and Y were global while the equation ran; leave nothing behind.
    (setq x nil y nil)
    (Surf:Restore)
    (princ)
)

(princ)
