XData Refactor Plan — Eliminate Text File Serialization

Created: February 24, 2026
Status: Planned — ready to implement
Priority: High
Affects: inspanel.lsp, tiltup.lsp, savelay.lsp, layout.lsp


Problem Statement

The panel workflow currently stores entity transformation data in text files (tiltlist.txt, conslist.txt) using prin1/read serialization. This approach is fundamentally broken:

  1. ENAME serialization failure (Bug 14): entget returns ENAME objects (DXF group 330 owner pointer) that prin1 serializes as <Entity name: 7ef73060> — a string that read cannot parse back. Even with filtering, this is fragile.

  2. Index mismatch bugs (Bug 15): The text file stores a flat list with no entity identity. Matching saved data back to live entities requires block-name lookup loops that are error-prone (nn vs xn index confusion).

  3. File deletion race (Bug 13 investigation): inspanel line 33 runs del *list.txt which destroys all *list.txt files in the project folder before any recovery code can read them.

  4. No atomicity: If AutoCAD crashes mid-workflow, the text files may be corrupt or absent, with no recovery path.

AutoCAD 2000 (R15) has supported Extended Entity Data (XData) since its AutoCAD R11 roots (1990). XData stores application-specific data directly on entities in the drawing database — no external files, no serialization, no parsing. It survives save/load cycles, copy/paste, and WBLOCK operations.


Design

Storage Approach: XData on INSERT Entities

Each panel is an INSERT (xref) entity on layer 0. We store transformation snapshots as XData directly on these entities using registered application names.

App Name

Purpose

Written By

Read By

CSV_TILT

Pre-layout (construction) transformation

inspanel.lsp

tiltup.lsp

CSV_LAYOUT

Pre-tiltup (laid-flat) transformation

savelay.lsp

layout.lsp

XData Structure

Each app’s XData stores the complete transformation state needed to restore an INSERT entity:

(-3 ("CSV_TILT"
  (1002 . "{")
  (1010 . (x y z))       ;; insertion point (group 10)
  (1040 . x_scale)       ;; X scale factor (group 41)
  (1040 . y_scale)       ;; Y scale factor (group 42)
  (1040 . z_scale)       ;; Z scale factor (group 43)
  (1040 . rotation)      ;; rotation angle in radians (group 50)
  (1010 . (nx ny nz))    ;; extrusion direction / OCS normal (group 210)
  (1002 . "}")
))

XData type codes used:

Code

Type

Purpose

1002

Control string

{ open, } close group

1010

3D point

Insertion point, extrusion

1040

Real

Scale factors, rotation

Why XData (Not Dictionaries or XRecords)

  • XData travels with the entity — if the INSERT is copied, moved, or WBLOCKed, its XData goes with it. Dictionary XRecords are drawing-global and don’t follow entities.

  • AutoCAD 2000 full support — XData predates R15 by a decade. regapp, entget with app filter, entmod with XData all work.

  • No file I/O — eliminates open/close/read/prin1 and all the failure modes that come with text file round-tripping.

  • Survives crashes — XData is saved with the drawing. If AutoCAD crashes after entmod but before a text file write completes, the text file approach loses data. XData persists in the undo stack and in the saved .dwg.


Current Code Analysis

inspanel.lsp — Writes tiltlist.txt

Normal path (lines 470–500): After attaching all panel xrefs, collects all INSERT entities on layer 0, strips ENAME/XData from each entity’s DXF data, writes the filtered list to tiltlist.txt via prin1.

Fast path (lines 105–145): When panels are already attached (no wall lines, no dictionary, but INSERT entities exist), does the same ENAME-filtering write to generate tiltlist.txt.

What it captures: The entity state after xref attachment — panels are in their 3D construction position along walls. This is the state that Tilt-Up needs to restore after Layout flattens them.

tiltup.lsp — Reads tiltlist.txt

Reads tiltlist.txt via (read (read-line f)). Builds pnllst from live INSERT entities. Matches by block name (DXF group 2). Substitutes live entity name (group -1) into saved data. Calls (entmod) on each entry to restore the construction position.

Key insight: Tiltup doesn’t need the entire association list — it only needs groups 10 (insertion pt), 41/42/43 (scale), 50 (rotation), and 210 (extrusion) to restore the transformation. The current approach carries ~20 DXF groups per entity when only 6 matter.

savelay.lsp — Writes conslist.txt

10 lines. Collects all INSERT entities on layer 0 via ssget. Strips entity names via (mapcar '(lambda (x) (cdr x)) pnllst) — removes the (-1 . ename) pair. Writes to conslist.txt via prin1.

Called by: layout.lsp first pass (after rotating panels flat).

What it captures: The entity state after Layout rotates panels from 3D wall position to flat layout position. This is what Layout’s second pass restores to undo the layout operation.

layout.lsp — Reads conslist.txt

Two-pass design based on file existence:

  • Pass 1 (conslist.txt does NOT exist): First-time layout. Rotates each INSERT from 3D construction position to flat. Transforms: rotation = angle of OCS normal + 90°, insertion point translated from OCS to WCS, extrusion set to (0 0 1). Then calls (savelay) to snapshot the result.

  • Pass 2 (conslist.txt EXISTS): Undo layout. Reads saved data, matches by block name (same loop pattern as tiltup), substitutes live entity names, calls (entmod) to restore.

Same bug pattern as tiltup: Uses (cons (assoc -1 (nth nn pnllst)) (nth nn conslst)) — the nn vs xn index bug likely exists here too (untested, but the code structure is identical).


Implementation Plan

Step 1: Create XData Helper Functions

Create a small utility (or add to an existing utility file) with reusable functions:

;;;------------------------------------------------------------
;;; csv_xdata.lsp - XData save/restore helpers
;;;
;;; Provides functions to store and retrieve panel
;;; transformation snapshots as XData on INSERT entities.
;;;------------------------------------------------------------

(defun csv_save_xdata (ename appname / e ins xs ys zs rot ext xd)
  ;;
  ;; Save the current transformation of entity ename
  ;; as XData under the given application name.
  ;;
  (regapp appname)
  (set 'e (entget ename))
  (set 'ins (cdr (assoc 10 e)))     ;; insertion point
  (set 'xs  (cdr (assoc 41 e)))     ;; x scale
  (set 'ys  (cdr (assoc 42 e)))     ;; y scale
  (set 'zs  (cdr (assoc 43 e)))     ;; z scale
  (set 'rot (cdr (assoc 50 e)))     ;; rotation
  (set 'ext (cdr (assoc 210 e)))    ;; extrusion direction
  (set 'xd
    (list -3
      (list appname
        '(1002 . "{")
        (cons 1010 ins)
        (cons 1040 xs)
        (cons 1040 ys)
        (cons 1040 zs)
        (cons 1040 rot)
        (cons 1010 ext)
        '(1002 . "}")
      )
    )
  )
  ;; If entity already has XData for this app, remove it first
  (set 'e (entget ename (list appname)))
  (if (assoc -3 e)
    (set 'e (subst xd (assoc -3 e) e))
    (set 'e (append e (list xd)))
  )
  (entmod e)
)

(defun csv_restore_xdata (ename appname / e xd vals ins xs ys zs rot ext)
  ;;
  ;; Read XData from entity and restore the saved
  ;; transformation groups via entmod.
  ;; Returns T if successful, nil if no XData found.
  ;;
  (set 'e (entget ename (list appname)))
  (set 'xd (assoc -3 e))
  (if (not xd)
    nil
    (progn
      ;; Extract values from XData group list
      ;; Structure: (-3 ("APPNAME" (1002 . "{") (1010 . ins) (1040 . xs) (1040 . ys) (1040 . zs) (1040 . rot) (1010 . ext) (1002 . "}")))
      (set 'vals (cdadr xd))  ;; skip appname string
      ;; Walk the XData values in order
      (set 'vals (cdr vals))  ;; skip opening "{"
      (set 'ins (cdr (car vals)))   (set 'vals (cdr vals))
      (set 'xs  (cdr (car vals)))   (set 'vals (cdr vals))
      (set 'ys  (cdr (car vals)))   (set 'vals (cdr vals))
      (set 'zs  (cdr (car vals)))   (set 'vals (cdr vals))
      (set 'rot (cdr (car vals)))   (set 'vals (cdr vals))
      (set 'ext (cdr (car vals)))
      ;; Apply to entity
      (set 'e (entget ename))
      (set 'e (subst (cons 10 ins)  (assoc 10 e)  e))
      (set 'e (subst (cons 41 xs)   (assoc 41 e)  e))
      (set 'e (subst (cons 42 ys)   (assoc 42 e)  e))
      (set 'e (subst (cons 43 zs)   (assoc 43 e)  e))
      (set 'e (subst (cons 50 rot)  (assoc 50 e)  e))
      (set 'e (subst (cons 210 ext) (assoc 210 e) e))
      (entmod e)
      T
    )
  )
)

(defun csv_has_xdata (ename appname / e)
  ;;
  ;; Check if entity has XData for the given app.
  ;; Returns T or nil.
  ;;
  (set 'e (entget ename (list appname)))
  (if (assoc -3 e) T nil)
)

Step 2: Rewrite inspanel.lsp — Save XData Instead of tiltlist.txt

Replace the tiltlist.txt write block (both normal and fast paths) with:

;; Save construction-position transformation as XData
;; on each panel INSERT entity
(set 'xn (sslength xls))
(while (> xn 0)
  (set 'xn (1- xn))
  (csv_save_xdata (ssname xls xn) "CSV_TILT")
)

Also remove the del *list.txt shell command (line 33) or change it to only delete specific non-XData files if any are still needed.

Fast path: Same change — call csv_save_xdata instead of writing tiltlist.txt.

Step 3: Rewrite tiltup.lsp — Read XData Instead of tiltlist.txt

Replace the entire file read + block-name matching + entity-name substitution with:

(defun tiltup ()
  (set 'xls
    (ssget "x"
      '((-4 . "<and") (0 . "INSERT") (8 . "0") (-4 . "and>"))
    )
  )
  (if (not xls)
    (alert "No panel entities found in the drawing.")
    (progn
      (set 'xn (sslength xls))
      (set 'count 0)
      (while (> xn 0)
        (set 'xn (1- xn))
        (if (csv_restore_xdata (ssname xls xn) "CSV_TILT")
          (set 'count (1+ count))
        )
      )
      (if (= count 0)
        (alert
          (strcat
            "No CSV_TILT data found on panel entities.\n\n"
            "You must Attach Panels before Tilt-Up."
          )
        )
      )
    )
  )
)

Benefits:

  • No file I/O at all

  • No block-name matching loops

  • No entity-name substitution gymnastics

  • Each entity carries its own restoration data

  • Works even if panels are added/removed between Layout and Tilt-Up

Step 4: Rewrite savelay.lsp — Save XData Instead of conslist.txt

(defun savelay ()
  (set 'xls
    (ssget "x"
      '((-4 . "<and") (0 . "INSERT") (8 . "0") (-4 . "and>"))
    )
  )
  (if xls
    (progn
      (set 'xn (sslength xls))
      (while (> xn 0)
        (set 'xn (1- xn))
        (csv_save_xdata (ssname xls xn) "CSV_LAYOUT")
      )
    )
  )
)

Step 5: Rewrite layout.lsp — Check XData Instead of conslist.txt

Pass detection: Instead of (findfile "conslist.txt"), check if any INSERT entity has CSV_LAYOUT XData:

;; Check if layout has already been run (entities have CSV_LAYOUT XData)
(set 'xls
  (ssget "x"
    '((-4 . "<and") (0 . "INSERT") (8 . "0") (-4 . "and>"))
  )
)
(set 'has_layout nil)
(if xls
  (if (csv_has_xdata (ssname xls 0) "CSV_LAYOUT")
    (set 'has_layout T)
  )
)

Pass 1 (first time): Same rotation logic as current code, then call (savelay) which now writes XData.

Pass 2 (undo layout): Replace file read + matching with:

(set 'xn (sslength xls))
(while (> xn 0)
  (set 'xn (1- xn))
  (csv_restore_xdata (ssname xls xn) "CSV_LAYOUT")
)

Step 6: Module Loading

Add csv_xdata.lsp to the module load list in csv.lsp so the helper functions are available. It needs to load before inspanel, tiltup, savelay, and layout.


Migration / Backward Compatibility

Existing Drawings

Drawings saved before this refactor will have no XData on their INSERT entities. The code must handle this gracefully:

  • tiltup with no CSV_TILT XData: Show alert “You must Attach Panels before Tilt-Up” (same UX as current “tiltlist.txt not found” message).

  • layout with no CSV_LAYOUT XData: Treat as first-time layout (Pass 1) — same as current behavior when conslist.txt doesn’t exist.

  • inspanel on already-attached panels: The fast path calls csv_save_xdata on all existing INSERTs, adding CSV_TILT XData for future Tilt-Up use.

Text Files

After this refactor, tiltlist.txt and conslist.txt are no longer written or read. The del *list.txt command in inspanel can be removed entirely (or kept as cleanup for legacy files in existing project folders).


Files Modified

File

Change

csv_xdata.lsp

NEW — XData helper functions

inspanel.lsp

Replace tiltlist.txt write with csv_save_xdata

tiltup.lsp

Replace tiltlist.txt read with csv_restore_xdata

savelay.lsp

Replace conslist.txt write with csv_save_xdata

layout.lsp

Replace conslist.txt read with csv_restore_xdata

csv.lsp

Add csv_xdata.lsp to module load list


Risks

  1. XData size limit: AutoCAD limits XData to 16 KB per entity per app. Each panel stores ~6 values (~100 bytes). Even with 500 panels, this is well under the limit.

  2. XData and XREF reloads: When an xref is detached and re-attached, its INSERT entity is recreated — XData is lost. This is fine because inspanel (which attaches xrefs) is also what writes CSV_TILT XData.

  3. Undo: entmod with XData participates in AutoCAD’s undo system. UNDO after inspanel would remove the XData. This is the same risk as the current text file approach (undo doesn’t un-delete a text file either).


Testing Plan

  1. Attach Panels on CSBsite1 → verify CSV_TILT XData appears on INSERT entities (check via (entget (ssname xls 0) '("CSV_TILT")))

  2. Layout Panels → verify entities rotate flat, CSV_LAYOUT XData written, CSV_TILT XData preserved

  3. Layout Panels again (Pass 2) → verify entities return to laid-flat position from CSV_LAYOUT XData

  4. Tilt-Up Panels → verify entities return to 3D construction position from CSV_TILT XData

  5. Save and reopen → verify XData survives drawing save/load cycle

  6. No tiltlist.txt or conslist.txt should exist in the project folder after any operation