;;; ---------------------------------------------------------------------------
;;; HpglImport.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; READ AN HPGL PLOT FILE BACK INTO A DRAWING
;;;
;;; PURPOSE
;;;   Turns a plot file back into geometry. HPGL - Hewlett Packard Graphics
;;;   Language - is what a drawing became when it was sent to a pen plotter, and
;;;   a great many drawings survive only as .plt files because the original DWG
;;;   was lost, was on a machine long gone, or was never handed over.
;;;
;;;   What comes back is polylines, not the original objects: a plot file records
;;;   pen strokes, so a circle arrives as the short straight segments the plotter
;;;   drew it with. It is a tracing, not a recovery. But it is to scale, and it
;;;   beats scanning a print.
;;;
;;; THE FORMAT
;;;   Commands are two letters, then arguments, then a semicolon:
;;;
;;;     IN;          initialise
;;;     SP2;         select pen 2
;;;     PU1000,2000; pen up, move to
;;;     PD3000,2000; pen down, draw to
;;;     PA...;       plot absolute - move or draw depending on pen state
;;;
;;;   Coordinates are in plotter units. The usual is 1016 to the inch, which is
;;;   40 to the millimetre, and that is what is assumed - it can be changed.
;;;
;;;   Each pen becomes a layer, so the colour separation the plot was set up
;;;   with survives the trip back.
;;;
;;; WHAT WAS FIXED
;;;   - It read the file one character at a time through a hand-written state
;;;     machine, and never checked for the end of the file. READ-CHAR returns nil
;;;     at the end, (chr nil) is a bad argument, and every run ended by stopping
;;;     with an error rather than by finishing.
;;;   - The file was left open when that happened - and stayed open, locked,
;;;     until AutoCAD was closed.
;;;   - Pen widths were mapped from a table of twelve hard-coded values in
;;;     inches, applied as polyline widths, so on a metric drawing every line
;;;     came in twenty-five times too thin.
;;;   - Nine variables were global.
;;;   - It ran ZOOM to a fixed centre before reading anything, and had no error
;;;     handler.
;;;
;;;   HPGLIN  - read an HPGL plot file into the drawing
;;; ---------------------------------------------------------------------------

(setq *Hpgl:Units* nil)

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

;;; Split a string on a single character.
(defun Hpgl:Split ( s sep / i ch cur out )
    (setq i 0 cur "" out nil)
    (while (< i (strlen s))
        (setq i (1+ i) ch (substr s i 1))
        (if (= ch sep)
            (setq out (cons cur out) cur "")
            (setq cur (strcat cur ch))))
    (if (/= cur "") (setq out (cons cur out)))
    (reverse out)
)

;;; The numbers out of an argument string, as pairs of coordinates scaled into
;;; drawing units.
(defun Hpgl:Points ( args scale org / n out )
    (setq n (mapcar 'atof (Hpgl:Split args ",")))
    (while (> (length n) 1)
        (setq out (cons (list (+ (car org) (/ (car n) scale))
                              (+ (cadr org) (/ (cadr n) scale)))
                        out)
              n (cddr n)))
    (reverse out)
)

(defun c:HPGLIN ( / *error* vars vals path fh text line cmds c op args
                    scale org pen up here run layer n polys strokes v )

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

    (defun Hpgl:Restore ( )
        (if fh (vl-catch-all-apply 'close (list fh)))
        (setq fh nil)
        (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 )
        (Hpgl:Restore)
        (if (and msg (not (wcmatch (strcase msg t) "*break*,*cancel*,*exit*")))
            (princ (strcat "\n** HPGLIN 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 path (getfiled "HPGL plot file to read" "" "plt;hpg;hpgl;prn;*" 4))

    (if (null path)
        (princ "\nCancelled.")
        (progn
            (initget 6)
            (setq scale (getreal (strcat "\nPlotter units per drawing unit <"
                                         (rtos (cond (*Hpgl:Units*) (t 1016.0)) 2 1)
                                         ">"
                                         "\n  (1016 for inches, 40 for millimetres): ")))
            (if (null scale) (setq scale (cond (*Hpgl:Units*) (t 1016.0))))
            (setq *Hpgl:Units* scale)

            (setq org (getpoint "\nWhere to put the plot origin <0,0>: "))
            (if (null org) (setq org '(0.0 0.0 0.0)))

            ;; --- read the whole file --------------------------------------
            ;; All at once rather than a character at a time, so the end of the
            ;; file is simply the end of the text and not an error.
            (setq fh (open path "r") text "")
            (if (null fh)
                (princ (strcat "\n** Cannot open " path " **"))
                (progn
                    (while (setq line (read-line fh))
                        (setq text (strcat text line)))
                    (close fh)
                    (setq fh nil)

                    (setq cmds    (Hpgl:Split text ";")
                          pen     1
                          up      t
                          here    nil
                          run     nil
                          polys   0
                          strokes 0
                          layer   (Hpgl:Layer "HPGL-Pen1" 1))

                    (foreach c cmds
                        ;; Strip whitespace, then split into the two-letter
                        ;; instruction and its arguments.
                        (while (and (> (strlen c) 0)
                                    (member (substr c 1 1) '(" " "\t" "\r" "\n")))
                            (setq c (substr c 2)))
                        (if (>= (strlen c) 2)
                            (progn
                                (setq op   (strcase (substr c 1 2))
                                      args (substr c 3))

                                (cond
                                    ;; A new pen ends the current run and starts
                                    ;; a new layer.
                                    ((= op "SP")
                                     (if (> (length run) 1)
                                         (progn (Hpgl:Poly (reverse run) layer)
                                                (setq polys (1+ polys))))
                                     (setq run nil
                                           pen (max 1 (atoi args))
                                           layer (Hpgl:Layer
                                                     (strcat "HPGL-Pen" (itoa pen))
                                                     (1+ (rem (1- pen) 7)))))

                                    ;; Pen up: the run so far is finished.
                                    ((= op "PU")
                                     (if (> (length run) 1)
                                         (progn (Hpgl:Poly (reverse run) layer)
                                                (setq polys (1+ polys))))
                                     (setq run nil up t)
                                     (foreach p (Hpgl:Points args scale org)
                                         (setq here p)))

                                    ;; Pen down: start drawing from where the pen
                                    ;; is, and take in any points given with it.
                                    ((= op "PD")
                                     (setq up nil)
                                     (if here (setq run (list here)))
                                     (foreach p (Hpgl:Points args scale org)
                                         (setq run (cons p run) here p
                                               strokes (1+ strokes))))

                                    ;; Plot absolute: draws if the pen is down,
                                    ;; moves if it is up.
                                    ((member op '("PA" "PR"))
                                     (foreach p (Hpgl:Points args scale org)
                                         (if up
                                             (setq here p)
                                             (progn
                                                 (if (null run) (setq run (list here)))
                                                 (setq run (cons p run) here p
                                                       strokes (1+ strokes))))))
                                ))))

                    ;; Whatever was still being drawn when the file ran out.
                    (if (> (length run) 1)
                        (progn (Hpgl:Poly (reverse run) layer)
                               (setq polys (1+ polys))))

                    (princ (strcat "\n" (itoa polys) " polylines from "
                                   (itoa strokes) " pen strokes."
                                   "\n  One layer per pen. ZOOM Extents to find it."))
                    (if (zerop polys)
                        (princ (strcat "\n  Nothing was drawn - the file may not be"
                                       " HPGL, or may use a dialect this does not"
                                       " read.")))))))

    (Hpgl:Restore)
    (princ)
)

(princ)
