;;; ---------------------------------------------------------------------------
;;; TurtleDraw.lsp
;;; code compiled by YZ August 2026
;;; ---------------------------------------------------------------------------
;;; TURTLE GRAPHICS AND SPACE-FILLING CURVES
;;;
;;; PURPOSE
;;;   A turtle that walks and turns, and two curves drawn with it.
;;;
;;;   HILBERT  the Hilbert curve - a single line that visits every square of a
;;;            grid without crossing itself, and does so in a way that keeps
;;;            nearby points on the line near each other on the page. Order 1 is
;;;            four cells, order 2 sixteen, order n is 4^n.
;;;   KOCH     the Koch snowflake - a triangle with a smaller triangle raised on
;;;            the middle third of every edge, over and over.
;;;
;;;   Both are drawn as a single polyline, which is the point of them: a Hilbert
;;;   curve is a route, and cutting it or following it with a machine needs it
;;;   to be one object.
;;;
;;; WHY A HILBERT CURVE IS USEFUL
;;;   Not only decoration. It is the standard way to lay out a serpentine that
;;;   has to cover an area evenly - underfloor heating loops, hatching for a
;;;   pen plotter, a milling raster that avoids long returns - because it never
;;;   crosses itself and never makes a long jump.
;;;
;;; WHAT WAS FIXED
;;;   - The turtle moved by building relative coordinate STRINGS - "@12.34<90" -
;;;     and feeding them to the LINE command, one command call per step. An
;;;     order 5 Hilbert curve is a thousand separate LINE commands and a
;;;     thousand separate objects. It is now computed as points and drawn as one
;;;     polyline.
;;;   - Because each step was its own LINE, the curve could not be offset,
;;;     joined or followed as a path - the very things it exists for.
;;;   - Every turtle variable was global, and a list of nineteen function names
;;;     was assigned to a global called FILELIST at load time.
;;;   - It created layers named TURTLE-WHITE and so on as it went and left them.
;;;   - C:HILBERT declared DIST and SIGN as its own locals while the recursive
;;;     routine that used them was a separate function - it worked only because
;;;     AutoLISP looks names up dynamically, and would break under any other
;;;     LISP.
;;;   - It ended by returning nil rather than (princ).
;;;
;;;   HILBERT  - draw a Hilbert curve
;;;   KOCH     - draw a Koch snowflake
;;; ---------------------------------------------------------------------------

;;; ---------------------------------------------------------------------------
;;; THE TURTLE
;;;
;;; State is carried in the three variables below rather than in globals. Each
;;; step adds a point to the trail; nothing is drawn until the walk is done.
;;; ---------------------------------------------------------------------------

(defun Turtle:Start ( pt heading )
    (setq *T:Pos* pt *T:Ang* heading *T:Trail* (list pt))
)

(defun Turtle:Forward ( d )
    (setq *T:Pos*   (polar *T:Pos* *T:Ang* d)
          *T:Trail* (cons *T:Pos* *T:Trail*))
)

(defun Turtle:Turn ( deg )
    (setq *T:Ang* (+ *T:Ang* (/ (* pi deg) 180.0)))
)

(defun Turtle:Trail ( ) (reverse *T:Trail*))

;;; ---------------------------------------------------------------------------
;;; THE CURVES
;;; ---------------------------------------------------------------------------

;;; The Hilbert curve, drawn by the classic pair of mutually recursive rules.
;;; SIGN says which way this level turns; flipping it at each level is what
;;; makes the curve fold back into itself rather than spiral away.
(defun Turtle:Hilbert ( order dist sign )
    (if (> order 0)
        (progn
            (Turtle:Turn (* sign 90.0))
            (Turtle:Hilbert (1- order) dist (- sign))
            (Turtle:Forward dist)
            (Turtle:Turn (* (- sign) 90.0))
            (Turtle:Hilbert (1- order) dist sign)
            (Turtle:Forward dist)
            (Turtle:Hilbert (1- order) dist sign)
            (Turtle:Turn (* (- sign) 90.0))
            (Turtle:Forward dist)
            (Turtle:Hilbert (1- order) dist (- sign))
            (Turtle:Turn (* sign 90.0))))
)

;;; One edge of a Koch snowflake: the middle third replaced by two sides of a
;;; triangle, applied recursively to each of the four pieces that leaves.
(defun Turtle:Koch ( order dist )
    (if (zerop order)
        (Turtle:Forward dist)
        (progn
            (Turtle:Koch (1- order) (/ dist 3.0))
            (Turtle:Turn 60.0)
            (Turtle:Koch (1- order) (/ dist 3.0))
            (Turtle:Turn -120.0)
            (Turtle:Koch (1- order) (/ dist 3.0))
            (Turtle:Turn 60.0)
            (Turtle:Koch (1- order) (/ dist 3.0))))
)

(defun Turtle: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 Turtle: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)))
)

;;; ---------------------------------------------------------------------------
;;; COMMANDS
;;; ---------------------------------------------------------------------------

(defun Turtle:Run ( which / *error* vars vals org order dist lay pts n v )

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

    (defun Turtle:Restore ( )
        (setq *T:Pos* nil *T:Ang* nil *T:Trail* 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 )
        (Turtle:Restore)
        (if (and msg (not (wcmatch (strcase msg t) "*break*,*cancel*,*exit*")))
            (princ (strcat "\n** TURTLE 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 6)
    (setq order (getint (strcat "\nOrder <" (if (= which "Hilbert") "4" "3") ">: ")))
    (if (null order) (setq order (if (= which "Hilbert") 4 3)))
    (if (> order (if (= which "Hilbert") 7 5))
        (progn
            (princ "\n  That would take a very long time. Reduced.")
            (setq order (if (= which "Hilbert") 7 5))))

    ;; Say how big it will be before drawing any of it.
    (setq n (if (= which "Hilbert")
                (fix (expt 4.0 order))
                (* 3 (fix (expt 4.0 order)))))
    (princ (strcat "\n  " (itoa n) " segments."))

    (initget 6)
    (setq dist (getdist "\nLength of one segment: "))
    (setq org (getpoint "\nStarting corner: "))

    (if (or (null dist) (null org))
        (princ "\nCancelled.")
        (progn
            (setvar "OSMODE" 0)
            (setq org (list (car org) (cadr org) 0.0)
                  lay (Turtle:Layer "Curve" 4))

            (Turtle:Start org 0.0)

            (if (= which "Hilbert")
                (Turtle:Hilbert order dist 1.0)
                ;; A snowflake is three Koch edges with a 120 degree turn
                ;; between them.
                (repeat 3
                    (Turtle:Koch order (* dist (expt 3.0 order)))
                    (Turtle:Turn -120.0)))

            (setq pts (Turtle:Trail))
            (Turtle:Poly pts lay (= which "Koch"))

            (princ (strcat "\n" which " of order " (itoa order) " - "
                           (itoa (length pts)) " vertices, one polyline."))))

    (Turtle:Restore)
    (princ)
)

(defun c:HILBERT ( ) (Turtle:Run "Hilbert"))
(defun c:KOCH    ( ) (Turtle:Run "Koch"))

(princ)
