Start

Your first button

Build a working AutoCAD tool from an empty folder, then run it from the ribbon, from the command line and from AutoLISP.

This page builds one real tool end to end. Every later page goes deeper on one part of what happens here, so it is worth typing along rather than skimming.

The tool counts objects: the ones you have selected, or everything in model space. Small, but it exercises the whole path: discovery, metadata, the command it becomes, the pyacad library, arguments in and a result out.

Before you start#

You need AutoCAD 2021 to 2026 with pyACAD installed: a pyACAD tab on the ribbon tells you it loaded. You also need a folder that pyACAD scans for extensions. You have two choices.

The quick way: use the folder the installer already scans, %APPDATA%\pyACAD\extensions. Anything you put there loads with no further setup. Put your work in a folder of its own beside pyACAD.extension, never inside it: updates replace pyACAD.extension whole.

The tidy way: keep your tools in a folder you own, for example D:\CadTools, and tell pyACAD about it. This guide assumes that folder. Create it, then register it one of three ways:

  • Settings window. Click Settings on the pyACAD panel. Under Where extensions are loaded from, click Add, pick D:\CadTools, then Save. Answer yes when it offers to reload pyACAD.
  • config.json. Add the folder to the "extensions" array in %APPDATA%\pyACAD\config.json. Backslashes are doubled in JSON.
  • Command line. From a terminal: %APPDATA%\pyACAD\cli\pyacad.exe extensions add D:\CadTools, then ... extensions list to confirm.
%APPDATA%\pyACAD\config.json
{
  "extensions": [
    "D:\\CadTools"
  ]
}

The folders listed there are scanned first, in order; the installer's %APPDATA%\pyACAD\extensions is still scanned after them, so the pyACAD tab does not go anywhere. If config.json already has other keys, add "extensions" beside them rather than replacing the file.

Step 1: create the folders#

Four nested folders, and the suffixes are what matter. The names before the suffixes are yours and become the labels you see.

D:\CadTools
D:\CadTools\
  CadTools.extension\
    CadTools.tab\
      Drafting.panel\
        Count.pushbutton\
          script.py

Create them in Explorer or in one line of PowerShell:

powershell
New-Item -ItemType Directory -Force `
  D:\CadTools\CadTools.extension\CadTools.tab\Drafting.panel\Count.pushbutton
Watch the suffixes

.extension, .tab, .panel and .pushbutton are matched literally, ignoring case: Count.PushButton works, Count.pushbuttons does not. A misspelled bundle suffix inside a panel is reported in a toast after the scan. A misspelled .extension, .tab or .panel is the silent one: the scan only looks for those exact patterns, so a wrong one is never looked at.

Step 2: write the script#

Create script.py inside Count.pushbutton:

Count.pushbutton\script.py
"""Counts the selected objects, or everything in model space."""
from pyacad import sel, toast

picked = sel.current()                    # the pickfirst set, as a list of handles
if picked:
    toast.success('%d object(s) selected' % len(picked))
else:
    every = sel.get(space='model')
    toast.info('Nothing selected', '%d object(s) in model space.' % len(every))

Three things are already decided that you did not configure:

  • The button label is Count, from the folder name.
  • The tooltip is the module docstring on the first line.
  • The engine is IronPython, the default.

And one more, which is the important one: the button is also the AutoCAD command PYCOUNT, named from its title.

Step 3: load it#

If AutoCAD is not running, start it. If it is, click Reload on the pyACAD panel or type PYACADRELOAD. A new CadTools tab appears with a Drafting panel and a Count button on it.

Open a drawing, select a few objects and click Count. A toast appears in the corner of the AutoCAD window with the count. Press Esc to clear the selection and click again: now it counts model space. Watch the command line as you click, and you will see _.PYCOUNT echoed there: the button sent its command, exactly as an Autodesk button does.

Nothing appeared

Look for a warning toast reading N bundle problems found. pyACAD shows one after any scan that had to skip something, quoting the first problem; the rest are in %APPDATA%\pyACAD\logs\pyACAD.log. The usual causes are a typo in a bundle suffix or a missing script.py. A root that is not registered, or an extension folder one level too deep inside it, produces no toast and no log line at all, because the scan never reaches it. Troubleshooting covers the rest.

Step 4: give it a proper title#

The folder name is a poor title once you want spaces, and the docstring is a poor tooltip once you want more than a sentence. Add a bundle.yaml beside the script:

Count.pushbutton\bundle.yaml
title: Count Objects
tooltip: Counts the selected objects, or every object in model space when nothing is selected.
context: doc

Click Reload. The button now reads Count Objects, the tooltip is yours, and context: doc greys the button out while no drawing is open. Because the title changed, so did the command: it is now PYCOUNTOBJECTS, the title's letters and digits uppercased behind PY. The old PYCOUNT is gone after the Reload.

Title and tooltip resolve in a fixed order, most specific first:

bundle.yaml title: / tooltip: if absent script.py __title__ / docstring if absent Count.pushbutton folder name (title only) The command is PY plus the resolved title's letters and digits, uppercased: Count Objects → PYCOUNTOBJECTS unless bundle.yaml sets command:
A tooltip has no third fallback: with no yaml tooltip and no docstring, the button simply has none.

If you want a command name that does not move when the title does, set it yourself with command: PYCOUNTOBJ. That value is used as written (uppercased, with anything but letters, digits, _ and - dropped); no PY is added, so pick a name nothing else in AutoCAD uses. The bundle.yaml reference lists every key.

Step 5: add an icon#

Without an icon the button renders as text only. Drop a 96×96 PNG named icon.png into the bundle folder and Reload.

folders
Count.pushbutton\
  script.py
  bundle.yaml
  icon.png            # 96x96, large buttons
  icon.dark.png       # 96x96, dark theme
  icon.small.png      # 32x32, stacks and menu rows
  icon.small.dark.png # 32x32, dark theme

Only icon.png is needed; the other three fall back to it. Supplying all four is what makes a tool look native in both themes and at both sizes. Icons covers the drawing rules.

Step 6: type it#

Clear the selection, click in the drawing, type PYCOUNTOBJECTS and press Enter. The same toast appears. This is not a second way of running the tool; it is the only way, and the button is a shortcut to it. That also means the command works anywhere AutoCAD takes a command:

  • a CUI macro on a tool palette or a menu: ^C^C_PYCOUNTOBJECTS;
  • a line in a .scr script file;
  • AutoLISP's (command "PYCOUNTOBJECTS"), although the bridge below is better when you want an answer back.

Select some objects first and type the command: the selection is still there when the script reads it, because pyACAD registers its commands with AutoCAD's pickfirst flag.

Step 7: take an argument and return a value#

A click passes no arguments. The loader's PYACAD command and the (pyacad ...) LISP function do: everything after the tool's name arrives in the script as the list __args__, and whatever the script leaves in __result__ goes back to the caller. Extend the script so an optional layer pattern narrows the count:

Count.pushbutton\script.py
"""Counts the selected objects, or everything in model space."""
from pyacad import script, sel, toast

args = script.get_args()                  # [] from a click; ['E-*'] from (pyacad ...)
layer = str(args[0]) if args else None    # a layer name, wildcards allowed

picked = [] if layer else sel.current()   # an argument means: ignore the selection
if picked:
    toast.success('%d object(s) selected' % len(picked))
    __result__ = len(picked)
else:
    found = sel.get(layer=layer, space='model')
    where = ('on layers ' + layer) if layer else 'in model space'
    toast.info('%d object(s) %s' % (len(found), where))
    __result__ = len(found)

No Reload is needed for an edit to script.py; the next run reads the new file. Try it from the command line with PYACAD, which prompts for a command (with or without the PY) and its arguments on one line:

AutoCAD command line
Command: PYACAD
Script path or pyACAD command: COUNTOBJECTS E-*

Arguments typed at that prompt arrive as strings, split on spaces; wrap one in double quotes to keep a space inside it.

Step 8: call it from AutoLISP#

The loader defines one LISP function, pyacad. Its first argument is a command name (or a .py path), the rest become __args__, and its return value is the script's __result__. Type this at the command line:

AutoCAD command line
Command: (setq n (pyacad "PYCOUNTOBJECTS" "E-*"))
42
Command: (princ (strcat "\nElectrical objects: " (itoa n)))
Electrical objects: 42

An integer comes back as a LISP integer, a float as a real, a string as a string, a list as a list and a dictionary as an association list of two-element lists. A script that sets no __result__, or fails, returns nil, and a failure is written to the log. That is enough to wire pyACAD tools into existing LISP routines without rewriting them. Commands, macros and AutoLISP has the full conversion table.

The bridge runs the bundle by name

PYACAD and (pyacad ...) look the bundle up in pyACAD's command table and run it directly, rather than sending the command, because that is the only way to hand it arguments and get a value back. The lookup ignores case and accepts the name with or without PY.

What you have built#

folders
D:\CadTools\                     # a registered extension root
  CadTools.extension\
    CadTools.tab\                # the CadTools ribbon tab
      Drafting.panel\            # the Drafting panel
        Count.pushbutton\        # the button, and the command PYCOUNTOBJECTS
          script.py              # what runs
          bundle.yaml            # title, tooltip, context
          icon.png               # 96x96

That is a complete pyACAD tool: clickable, typeable, scriptable and callable from LISP, with no compile step and no restart. From here, Shift+Click can run a config.py beside the script for the tool's options (and gets its own PYCOUNTOBJECTSCFG command), and shortcut: in bundle.yaml binds a keyboard chord.