;;; ---------------------------------------------------------------------------
;;; PneumaticLine.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; PNEUMATIC AND INSTRUMENT SIGNAL LINES
;;;
;;; PURPOSE
;;;   Draws the signal lines used on process and instrument diagrams, where the
;;;   marks along a line say what kind of signal it carries:
;;;
;;;     Pneumatic   double cross-hatch ticks    //  //  //
;;;     Electric    single dashes across        /   /   /
;;;     Capillary   crossed X marks             X   X   X
;;;     Data        small circles               o   o   o
;;;
;;;   Pick a route and the line is drawn with its marks evenly spaced along it,
;;;   each square to the line at that point.
;;;
;;; WHAT WAS FIXED
;;;   Two routines are folded in here, and each had the same fatal habit.
;;;
;;;   - One inserted blocks called "pneu0" and "pneu90" which had to already be
;;;     in the drawing. Neither is anywhere in the collection, so it drew a line
;;;     and then failed on the first insert.
;;;   - It chose between those two blocks by testing the line angle, then
;;;     subtracted 90 from the rotation to compensate - which put the marks
;;;     square to the line only on the four cardinal directions.
;;;   - The other had its mark size written into the code as 0.025 and 0.12, and
;;;     its spacing as 0.5, in drawing units. At any scale but the one it was
;;;     written for, the marks were either invisible or the length of the line.
;;;   - It set the spacing from a chain of five angle tests covering the four
;;;     quadrants, with the boundaries written as (/ (* 3 Pi) 4) and similar -
;;;     and the tests used > and < with no equals, so a line at exactly 45
;;;     degrees matched no branch at all and the mark angle was left at whatever
;;;     the previous line had set.
;;;   - Both left CMDECHO and BLIPMODE as they found them only on the path where
;;;     nothing went wrong, and one finished by returning its author's copyright
;;;     motto to the command line.
;;;
;;;   PNEUMLINE  - draw an instrument signal line
;;; ---------------------------------------------------------------------------

(vl-load-com)
(setq *Pneum:Size* nil *Pneum:Kind* nil)

(defun Pneu: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 Pneu:Line ( p1 p2 layer )
    (entmake (list '(0 . "LINE") (cons 8 layer) (cons 10 p1) (cons 11 p2)))
)

;;; One mark at PT, with the line running at ANG. Every mark is built from the
;;; line direction, so it stays square whatever way the line goes - which is
;;; what the block-swapping version could not manage.
(defun Pneu:Mark ( pt ang size kind layer / lean back )
    (setq lean (+ ang (/ pi 4.0))          ; ticks lean at 45 degrees
          back (- ang (/ pi 4.0)))
    (cond
        ((= kind "Pneumatic")
         ;; Two parallel ticks, offset either side of the point.
         (foreach d (list (* size 0.35) (* size -0.35))
             (Pneu:Line (polar (polar pt ang d) lean size)
                        (polar (polar pt ang d) lean (- size))
                        layer)))

        ((= kind "Electric")
         (Pneu:Line (polar pt lean size) (polar pt lean (- size)) layer))

        ((= kind "Capillary")
         (Pneu:Line (polar pt lean size) (polar pt lean (- size)) layer)
         (Pneu:Line (polar pt back size) (polar pt back (- size)) layer))

        ((= kind "Data")
         (entmake (list '(0 . "CIRCLE") (cons 8 layer)
                        (cons 10 pt) (cons 40 (* size 0.6)))))
    )
)

(defun c:PNEUMLINE ( / *error* vars vals pts p kind size spacing lay obj
                       cl total d n ang v )

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

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

    (initget "Pneumatic Electric Capillary Data")
    (setq kind (getkword (strcat "\nSignal [Pneumatic/Electric/Capillary/Data] <"
                                 (cond (*Pneum:Kind*) (t "Pneumatic")) ">: ")))
    (if (null kind) (setq kind (cond (*Pneum:Kind*) (t "Pneumatic"))))
    (setq *Pneum:Kind* kind)

    (initget 6)
    (setq size (getdist (strcat "\nMark size"
                                (if *Pneum:Size*
                                    (strcat " <" (rtos *Pneum:Size* 2 3) ">") "")
                                ": ")))
    (if (null size) (setq size *Pneum:Size*))

    (if (null size)
        (princ "\nNo mark size given.")
        (progn
            (setq *Pneum:Size* size)
            (initget 6)
            (setq spacing (getdist (strcat "\nSpacing between marks <"
                                           (rtos (* size 6.0) 2 3) ">: ")))
            (if (null spacing) (setq spacing (* size 6.0)))

            (princ "\nPick the route of the line.")
            (setq pts nil)
            (while (setq p (getpoint (if pts (car pts) nil)
                                     (strcat "\nPoint " (itoa (1+ (length pts)))
                                             (if (> (length pts) 1)
                                                 " <Enter when done>" "") ": ")))
                (setq pts (cons (list (car p) (cadr p) 0.0) pts)))
            (setq pts (reverse pts))

            (if (< (length pts) 2)
                (princ "\nTwo points at least are needed.")
                (progn
                    (setvar "OSMODE" 0)
                    (setq lay (Pneu:Layer "Signal-Line" 6))
                    (setvar "CLAYER" lay)

                    ;; The line itself, as one polyline.
                    (entmake (append
                        (list '(0 . "LWPOLYLINE") '(100 . "AcDbEntity")
                              (cons 8 lay) '(100 . "AcDbPolyline")
                              (cons 90 (length pts)) '(70 . 0))
                        (mapcar '(lambda ( q ) (cons 10 (list (car q) (cadr q)))) pts)))
                    (setq cl  (entlast)
                          obj (vlax-ename->vla-object cl)
                          total (vlax-curve-getDistAtParam obj
                                    (vlax-curve-getEndParam obj)))

                    ;; Marks stepped along by DISTANCE, so they stay evenly
                    ;; spaced round a corner instead of bunching at it.
                    (setq d (/ spacing 2.0) n 0)
                    (while (< d total)
                        (setq p   (vlax-curve-getPointAtDist obj d)
                              ang (angle '(0 0 0)
                                    (vlax-curve-getFirstDeriv obj
                                        (vlax-curve-getParamAtDist obj d))))
                        (Pneu:Mark p ang size kind lay)
                        (setq n (1+ n) d (+ d spacing)))

                    (princ (strcat "\n" (strcase kind t) " line "
                                   (rtos total 2 3) " long, "
                                   (itoa n) " mark" (if (= n 1) "" "s") "."))))))

    (Pneu:Restore)
    (princ)
)

(princ)
