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 listto confirm.
{
"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\
CadTools.extension\
CadTools.tab\
Drafting.panel\
Count.pushbutton\
script.pyCreate them in Explorer or in one line of PowerShell:
New-Item -ItemType Directory -Force `
D:\CadTools\CadTools.extension\CadTools.tab\Drafting.panel\Count.pushbutton.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:
"""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.
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:
title: Count Objects
tooltip: Counts the selected objects, or every object in model space when nothing is selected.
context: docClick 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:
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.
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 themeOnly 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
.scrscript 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:
"""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:
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:
Command: (setq n (pyacad "PYCOUNTOBJECTS" "E-*"))
42
Command: (princ (strcat "\nElectrical objects: " (itoa n)))
Electrical objects: 42An 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.
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#
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 # 96x96That 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.