;;; ---------------------------------------------------------------------------
;;; GridSurface.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; Reads a CSV of gridded levels and builds either a 3D mesh or a cloud of 3D
;;; points from it. Aimed at survey and site data.
;;;
;;; THE EXPECTED FILE FORMAT
;;; The CSV must be a plain rectangular grid of Z values - nothing else. Each
;;; line is one row of the grid, each comma-separated field is one Z value:
;;;
;;;     12.40,12.55,12.71,12.90
;;;     12.38,12.51,12.68,12.85
;;;     12.31,12.44,12.60,12.77
;;;
;;; There are no X and Y columns, because they are not needed: the position of
;;; a value within the grid supplies them. Column index times cell width gives
;;; X, row index times cell height gives Y, and the value itself is Z. You are
;;; asked for the cell size when the file loads.
;;;
;;; MESH OR POINTS
;;; A 3D polygon mesh is limited by AutoCAD to 256 vertices in each direction,
;;; so a grid larger than 255 x 255 can only be drawn as points. The routine
;;; checks this before offering the choice and says so plainly rather than
;;; failing later.
;;;
;;; Points are the safer option for very large or noisy data; a mesh is what
;;; you want if the surface is going to be rendered, sectioned or used as a
;;; drape. If you choose points and cannot see them afterwards, that is PDMODE
;;; - set it to 3 and PDSIZE to -1, then REGEN.
;;;
;;;   GRIDSURF  - import a CSV grid as a mesh or as 3D points
;;; ---------------------------------------------------------------------------

(vl-load-com)

;; AutoCAD's hard ceiling on 3D mesh vertices in either direction.
(setq GridSurface:MaxMesh 256)

;; ---------------------------------------------------------------------------
;; GridSurface:Split
;; ---------------------------------------------------------------------------
;; Splits a string on a single-character delimiter and returns a list of the
;; pieces.
;;
;; Written as a loop rather than the original's recursion. Recursion is elegant
;; here but it consumes one stack frame per field, and a survey file with a few
;; thousand columns will exhaust the stack and take the whole import down. The
;; loop has no such limit.
;;
;; str - [str] the line to split
;; chr - [str] single-character delimiter, e.g. ","
;; ---------------------------------------------------------------------------
(defun GridSurface:Split ( str chr / code pos result )
    (setq code   (ascii chr)
          result nil
    )
    (while (setq pos (vl-string-position code str))
        (setq result (cons (substr str 1 pos) result)
              str    (substr str (+ 2 pos))
        )
    )
    ;; Whatever remains after the last delimiter is the final field.
    (reverse (cons str result))
)

;; ---------------------------------------------------------------------------
;; GridSurface:ReadValue
;; ---------------------------------------------------------------------------
;; Converts one CSV field to a number, returning 0.0 for anything unreadable.
;;
;; The original applied read to every field, which is dangerous: read evaluates
;; the text as a LISP expression, so a stray field containing a symbol name
;; would come back as a symbol rather than a number and corrupt the mesh
;; silently. distof parses strictly as a number and returns nil on failure,
;; which can then be handled honestly.
;; ---------------------------------------------------------------------------
(defun GridSurface:ReadValue ( str / val )
    (if (setq val (distof (vl-string-trim " \t\r" str) 2))
        val
        0.0
    )
)

;; ---------------------------------------------------------------------------
;; GridSurface:ReadCSV
;; ---------------------------------------------------------------------------
;; Reads the whole file and returns a list of rows, each row a list of reals.
;;
;; The file handle is closed in every exit path. A file left open by a failed
;; import stays locked for the rest of the AutoCAD session.
;;
;; Blank lines are skipped rather than becoming empty rows, since a trailing
;; newline at the end of a file is extremely common and would otherwise add a
;; phantom row of zeros along one edge of the surface.
;; ---------------------------------------------------------------------------
(defun GridSurface:ReadCSV ( filename / handle line rows )
    (if (setq handle (open filename "r"))
        (progn
            (while (setq line (read-line handle))
                (if (/= "" (vl-string-trim " \t\r" line))
                    (setq rows
                        (cons (mapcar 'GridSurface:ReadValue
                                      (GridSurface:Split line ",")
                              )
                              rows
                        )
                    )
                )
            )
            (close handle)
            (reverse rows)
        )
    )
)

;; ---------------------------------------------------------------------------
;; GridSurface:Flatten
;; ---------------------------------------------------------------------------
;; Converts the row/column grid into the flat X Y Z X Y Z ... list that
;; vla-Add3DMesh expects in its safearray.
;;
;; Built with cons and reversed once at the end, rather than appending inside
;; the loop as the original did. Repeated append is quadratic - it copies the
;; entire accumulated list on every single cell - so a 200 x 200 grid meant
;; forty thousand copies of an ever-growing list, which is why the original
;; crawled on real survey data. This version is linear.
;;
;; grid - [list] list of rows, each a list of Z values
;; cell - [list] (width height) of one grid cell
;; ---------------------------------------------------------------------------
(defun GridSurface:Flatten ( grid cell / r c row val result )
    (setq r      0
          result nil
    )
    (foreach row grid
        (setq c 0)
        (foreach val row
            ;; Pushed in reverse order so that one final reverse restores the
            ;; correct X, Y, Z sequence.
            (setq result (cons val (cons (* r (cadr cell)) (cons (* c (car cell)) result)))
                  c      (1+ c)
            )
        )
        (setq r (1+ r))
    )
    (reverse result)
)

;; ---------------------------------------------------------------------------
;; GridSurface:DrawMesh
;; ---------------------------------------------------------------------------
;; Builds a quad-surface 3D mesh from the grid.
;;
;; vla-Add3DMesh needs its coordinates as a safearray of doubles, sized to
;; exactly rows * cols * 3 elements, which is why the flat list is produced
;; first and poured into the array in one go.
;; ---------------------------------------------------------------------------
(defun GridSurface:DrawMesh ( grid rows cols cell / doc space points mesh )
    (setq doc    (vla-get-ActiveDocument (vlax-get-acad-object))
          space  (vla-get-ModelSpace doc)
          points (vlax-make-safearray vlax-vbDouble (cons 0 (1- (* rows cols 3))))
    )
    (vlax-safearray-fill points (GridSurface:Flatten grid cell))

    (setq mesh (vla-Add3DMesh space rows cols points))

    ;; A quad surface mesh is the right type for gridded data - it interpolates
    ;; each cell as a four-sided patch rather than triangulating.
    (vla-put-Type mesh acQuadSurfaceMesh)
    (vla-Update   mesh)

    ;; Density controls how finely the mesh is displayed. Matching it to the
    ;; source grid means what you see is the data, not a smoothed approximation.
    (vla-put-MDensity mesh rows)
    (vla-put-NDensity mesh cols)

    (vla-Regen doc acActiveViewport)
    (princ)
)

;; ---------------------------------------------------------------------------
;; GridSurface:DrawPoints
;; ---------------------------------------------------------------------------
;; Creates one POINT entity per grid cell.
;;
;; entmakex is used in preference to running the POINT command per cell, which
;; is what the original's commented-out line would have done - forty thousand
;; command invocations against forty thousand direct entity creations is the
;; difference between minutes and seconds.
;; ---------------------------------------------------------------------------
(defun GridSurface:DrawPoints ( grid cell / r c row val )
    (setq r 0)
    (foreach row grid
        (setq c 0)
        (foreach val row
            (entmakex
                (list '(0 . "POINT")
                      (cons 10 (list (* c (car cell)) (* r (cadr cell)) val))
                )
            )
            (setq c (1+ c))
        )
        (setq r (1+ r))
    )
    (princ)
)

;; ---------------------------------------------------------------------------
;; c:GRIDSURF  -  main routine
;; ---------------------------------------------------------------------------
(defun c:GRIDSURF ( / *error* vars vals file grid rows cols cell mode )

    (setq vars '("CMDECHO")
          vals (mapcar 'getvar vars)
    )

    (defun GridSurface:Restore ( )
        (mapcar 'setvar vars vals)
        (if (= 8 (logand 8 (getvar "UNDOCTL")))
            (command "_.UNDO" "_End")
            (vl-catch-all-apply '(lambda ( ) (*pop-error-mode*)) '())
        )
        (princ)
    )

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

    (cond
        ;; getfiled flag 4 permits any extension typed by the user while still
        ;; offering the .csv and .txt filters.
        (   (not (setq file (getfiled "Select CSV grid file to import" "" "csv;txt" 4)))
            (princ "\n*Cancelled*")
        )

        (   (not (setq grid (GridSurface:ReadCSV file)))
            (princ "\nFile could not be read, or contained no data.")
        )

        (   t
            (setq rows (length grid)
                  cols (length (car grid))
            )
            (princ (strcat "\n" (itoa rows) " rows x " (itoa cols) " columns imported."))

            ;; getcorner returns the opposite corner of a rubber-banded box
            ;; from the origin, which is a neat way of picking an X and Y
            ;; dimension together. Enter accepts the 1 x 1 default.
            (setq cell (getcorner '(0 0) "\nSpecify grid cell size <1.0,1.0>: "))
            (if (not cell)
                (setq cell '(1.0 1.0))
            )

            ;; Decide what can be offered before offering it.
            (if (or (>= rows GridSurface:MaxMesh) (>= cols GridSurface:MaxMesh))
                (progn
                    (princ (strcat "\nGrid exceeds the " (itoa GridSurface:MaxMesh)
                                   " vertex mesh limit - drawing as points."
                           )
                    )
                    (setq mode "Points")
                )
                (progn
                    (initget "Mesh Points")
                    (setq mode (getkword "\nDraw a 3D Mesh or 3D Points? [Mesh/Points] <Points>: "))
                    (if (not mode) (setq mode "Points"))
                )
            )

            (if (= "Mesh" mode)
                (progn
                    (princ "\nBuilding mesh...")
                    (GridSurface:DrawMesh grid rows cols cell)
                )
                (progn
                    (princ (strcat "\nDrawing " (itoa (* rows cols)) " points..."))
                    (GridSurface:DrawPoints grid cell)
                )
            )

            (princ " complete.")
            (princ "\n(If the points are not visible, set PDMODE to 3, PDSIZE to -1 and REGEN.)")
        )
    )

    (GridSurface:Restore)
    (princ)
)

(princ)
