LibreCAD 3 embeds two scripting languages simultaneously: Lua (historical,
retained), and Python (phase 5 addition). Both are backed by the same
C++-side neutral scripting layer (lcscripting), so operations,
event listeners, and plugins written in one language interoperate with
the other with zero glue code — they share the EventBus, the
operation registry, the custom-entity plugin slots, and the CLI/menu/
toolbar dispatch.
For quick-start authoring examples see lcUILua/ (Lua) and lcUIPy/
(Python). This document is the architecture reference.
┌─────────────────────────────────────────────────────────────┐
│ User script │
│ ┌───────────────────┐ ┌──────────────────────────┐ │
│ │ Lua sources │ │ Python sources │ │
│ │ lcUILua/*.lua │ │ lcUIPy/**/*.py │ │
│ └────────┬──────────┘ └────────┬─────────────────┘ │
│ │ │ │
│ ┌────────▼──────────┐ ┌────────▼─────────────────┐ │
│ │ kaguya (Lua) │ │ pybind11 (Python) │ │
│ └────────┬──────────┘ └────────┬─────────────────┘ │
│ │ │ │
│ ┌────────▼──────────┐ ┌────────▼─────────────────┐ │
│ │ lcadluascript │ │ lcadpythonscript │ │
│ │ - lua callback │ │ - python callback │ │
│ │ - lua object │ │ - python object │ │
│ └────────┬──────────┘ └────────┬─────────────────┘ │
│ └────────────┬─────────────┘ │
│ │ │
│ ┌─────────▼───────────┐ │
│ │ lcscripting │ │
│ │ (neutral layer): │ │
│ │ ScriptCallback, │ │
│ │ ScriptObject, │ │
│ │ ScriptValue, │ │
│ │ EventBus │ │
│ └─────────┬───────────┘ │
│ │ │
│ ┌─────────▼───────────┐ │
│ │ lckernel │ │
│ │ (entities, ops, │ │
│ │ builders, geo) │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Namespaces are 1:1 between the two languages by design. Both expose
lc.entity.*, lc.builder.*, lc.operation.*, lc.geo.*,
lc.meta.*, lc.storage.*, lc.event.*, lc.maths.*. The GUI
surface is exposed as gui (Lua) or lcgui (Python — a Python module
can't be named gui alone under import without a namespace clash
with the gui global in some workflows).
A scripting-defined operation is a class with:
- A
nameattribute (string) — used as the registry key. - A
command_lineattribute (string OR dict) — the CLI text that runs the op. When a string, the op runs_init_defaulton invocation. When a dict, each key is a CLI text; the corresponding value is the init-method suffix (e.g.{ "ARC": "arc_default", "ARCELLIPSE": "arc" }runs_init_arc_defaultand_init_arcrespectively). - An
iconattribute (string, optional) — filename relative to the Qt resource:/icons/prefix. Plugin-supplied icons resolve via the file-path fallback inToolbarButton::changeIcon(phase 5 PR-5.4) —<plugin_dir>/icons/<name>is tried when the qrc lookup misses. - A
descriptionattribute (string, optional) — tooltip text. - A
menu_actionsattribute (dict, optional) —{ init_method: qt_action_object_name }wires each init method to a Qt menu action by object name. E.g.{ "default": "actionPoint" }connects_init_defaultto theactionPointQAction (used by Lua'sPointOperationsand Python'sPyPointOperations). - A
groupattribute (string, optional) — toolbar group under the "Creation" tab. The exact string "Dim" is remapped to "Dimensions" as a legacy alias. - An
_init_<method>method for each init entry point. Called byMainWindow::runOperationafter the resolver produces the class andinstantiate()runs the class's__init__(Python) or its_init(...)(Lua).
Python-specific: do NOT call super().__init__(...) inside
_init_default — runOperation's instantiate() already ran the
base's __init__ once. A second call re-runs registerEvents()
(when the base is CreateOperations) and silently double-registers
every event listener, producing phantom duplicate entities on every
subsequent click app-wide until process restart. See
lcUIPy/create_actions/point_operations.py for the correct pattern
(set instance state directly), and the regression guard
CreateOperationsBaseInitNotDoubleCalled in
unittest/python/pythonbindings_test.cpp.
Both languages autoregister operations at import time. The registration APIs are Python-only because Python needs a decorator shape to be idiomatic; Lua uses the global-class-name convention it always used.
| Purpose | Lua | Python |
|---|---|---|
| Register an operation | (implicit — top-level | @lc.register_operation decorator |
| class assignment scans | ||
| the Lua state's global | ||
| table) | ||
| Register an event listener | luaInterface:registerEvent(name, fn) |
lc.event.register(name, fn) |
| Deregister a listener | luaInterface:deleteEvent(name, fn) |
lc.event.deregister(name, fn) |
| Register a custom-entity plugin | registerPlugin(name, fn) (global) |
lc.register_plugin(name, fn) (phase 6) |
The C++-side event names + payload shapes (source: mainwindow.cpp
trigger* slot family). Every payload is a language-neutral Map
that materializes into a Lua table or a Python dict per callback.
| Event | Payload keys | Source slot |
|---|---|---|
mousePress |
widget: CadMdiChild* |
MainWindow::triggerMousePressed |
mouseRelease |
widget: CadMdiChild* |
MainWindow::triggerMouseReleased |
mouseMove |
widget: CadMdiChild* |
MainWindow::triggerMouseMoved |
selectionChanged |
widget: CadMdiChild* |
MainWindow::triggerSelectionChanged |
keyPressed |
key: int |
MainWindow::triggerKeyPressed |
point |
position: Coordinate |
MainWindow::triggerCoordinateEntered |
relativePoint |
position: Coordinate |
MainWindow::triggerRelativeCoordinateEntered |
number |
number: double |
MainWindow::triggerNumberEntered |
text |
text: string |
MainWindow::triggerTextEntered |
command |
command: string |
MainWindow::triggerCommandEntered |
operationFinished |
(empty) | MainWindow::operationFinished |
finishOperation |
(empty) | CliCommand::finishOperation |
Note: number arrives as a Python float (not int) — even for
integer-typed input. CliCommand::enterNumber(double) →
ScriptValue(Kind::Double) → toPyLocked's py::float_(). Type-check
with isinstance(n, (int, float)) and coerce via int(n) where an
integer is required.
Lua plugins live under lcUILua/plugins/<name>/plugin.lua, loaded
by PluginManager::loadPlugins() via a directory walk driven by the
plugin_path global set from lcUILua/CMakeLists.txt. The plugin is
dofile'd in the shared Lua state, so top-level class assignments
land in the operation registry immediately.
Python plugins live under lcUIPy/plugins/<name>/plugin.py,
loaded by loadPythonPluginsFromPathPy("gui") in LuaInterface::initLua.
Each plugin gets its own py::dict namespace with import lc; import lcgui as gui
pre-loaded + a LC_interface = "gui" global; @lc.register_operation
inside populates the process-global lc.operation_registry dict.
Load order is BEFORE OperationLoader::loadPythonOperations so the
CLI/menu/toolbar wiring walk sees plugin registrations (phase 5 PR-5.5
fixup — reverse of the Lua order because Python's decorator model
needs the registration to exist before the wiring walk).
The plugin.py scan is bounded to the plugin subdir only — it does
NOT recurse into sub-subdirs. For a plugin needing multiple files,
use standard Python packaging (__init__.py + siblings under the
same subdir).
Custom entities (script-defined entity classes with a persistence
hook) are stored in DXF as app-data led by APP_NAME_CODE 102 /
"LibreCAD", then codes 410 (plugin name) / 411 (entity name) and
470/471 param pairs. Both languages register through the same
process-global CustomEntityManager, so a DXF written by a Lua-defined
custom entity reopens correctly when only a Python plugin is loaded
(assuming the plugin name matches — that's the routing key).
The multi-window bug (~LuaInterface calls
LuaCustomEntityManager::removePlugins() — closing ONE window
deregistered plugins for ALL windows) is fixed in phase 6 PR-6.1 by
making plugin registration process-scoped.
Some registration APIs are Python-only because the language shape demands it:
@lc.register_operation— decorator idiom; Lua uses the global- assignment convention.lc.event.register(name, listener)— Python needs a free function to route through the process-global hook installed atMainWindowconstruction time. Lua's equivalent isluaInterface:registerEvent(name, fn).lc.register_plugin(name, fn)— Python needs an explicit call to the process-global manager; Lua'sregisterPlugin(name, fn)is a global function in the Lua state.
The method-style binding surface (b:appendEntity(l) in Lua,
b.appendEntity(l) in Python) is deliberate 1:1 name parity — a
non-pythonic choice, but recording the same API surface in both
languages so authoring one translates trivially to the other. A
Pythonic append_entity alias may land in a future phase; for now,
the camelCase preserves the reference-doc §Custom-entity contract and
minimizes maintenance burden across two language-facing bindings.
Structure.md— module layout includinglcadpythonscript+lcscripting.Building.md— build requirements including python3-dev.README.md— top-level project overview mentions scripting.lckernel/cad/operations/entityops.h— inline examples now language-neutral (both Lua and Python users read the same pseudocode).