A modular object explorer, search engine, and action toolkit for grandMA3.
The main idea is "Spotlight"/"Raycast" for GrandMA3.
This is a broken example and poorly made plugin purely made by vibe-coding with claude. If anyone wants to pick this up, go ahead and fork it and update it.
This should NOT be used in a show-critical showfile.
- Elevated UI mode is not implemented well.
- Prisms and photons framework is in its infancy and their behaviors are broken.
- Selections needs refinement.
- Loose-folder modules (
prisms/,photons/,user_modules/) do not load when the plugin is imported into a showfile — MA3 only embeds the XML-declared components. This is by design (they are meant as loose "USB-stick" drop-ins); seeTODO.md.
RayMA grabs the keyboard the same way MA's own patch/editor windows do: the overlay
sets OverrideKeybSC="Yes", which suspends MA's keyboard shortcuts (the ShCuts icon
turns red) and frees the physical keys so they land in RayMA's capture field. Focus is
taken natively with FindBestFocus — no synthetic mouse clicks, and no global
KeyboardShortcutsActive toggle to restore. Shortcuts come back automatically the moment
the window closes.
- Version: 8.0
- Target firmware: grandMA3 2.3.x
- Runtime: Lua 5.4
RayMA turns the MA3 object tree into a fast, keyboard-driven workspace: fuzzy search across the show file, a tree browser, a property inspector, extensible quick actions, a command line, a calculator, a file browser, a persistent shell terminal, and a full plugin framework for building your own tools.
Launches straight into an empty search with the module list grouped by family (Navigate / Tools / Selection). Status line shows the live object index count in the showfiles datapoools — here, 2959 objects.
Typing macr fuzzy-matches across all data pools. Hits are ranked by relevance and show name, type, pool number, datapool, and a short content summary (2 lines, 1 lines). Selected row is highlighted green.
Typing anything will match to object type or name.
Tree navigator starting at Root. Each row shows index, name, class, and children count — press Enter to drill down, Right to inspect properties, Left/Esc to go back up.
Tap-tempo meter with running average and actions to apply the result to a Speed master, reset, or read the current master value. Enter taps, Esc exits.
Toy user module that fetches random cat facts from a public API — included as a live example of async HTTP work from within a RayMA module.
Three-reel slot machine that spins through a palette of named colors and applies the winner (here Teal) to the current selection. Demonstrates animated, stateful UI inside a user module.
Breakdown of a loaded show: total object count and memory use, then per-category rows (Patch — Fixtures / Parameters / Universes; Objects — Groups / Worlds / Filters). Enter drills into any category.
and more...
The plugin is designed to live at:
<ProgramData>/MALightingTechnology/gma3_library/datapools/plugins/RayMA/
- Fork the plugin.
- Git clone your repo to the plugin folder to the path above.
- In grandMA3, import
RayMA.xmlvia the Plugins pool. - Attach claude to the RayMA Folder
- Discuss with claude how to improve and implement features
- commit and push updates to your repo.
- Create pull requests to this main repo.
No external dependencies. The terminal module will opportunistically use LuaSocket if available, but falls back to io.popen.
When iterating, use scripts/deploy.py to copy the working tree into the local MA3
onPC plugin DataPool instead of copying files by hand:
python3 scripts/deploy.py # deploy to the default gma3_library location
python3 scripts/deploy.py --dry-run # preview what would be copied
MA3_PLUGINS_DIR=/some/path python3 scripts/deploy.py # override the target dir
It mirrors RayMA.xml, RayMA.lua, and the core/ modules/ prisms/ photons/ user_modules/ trees into
<ProgramData>/MALightingTechnology/gma3_library/datapools/plugins/RayMA/
(macOS: ~/MALightingTechnology/...), skipping .git/.DS_Store junk. After it runs,
reload the show (or restart MA3 onPC) to pick up the changes.
Invoke the plugin with an optional argument:
| Argument | Behavior |
|---|---|
| (none) | Open the search home screen (default) |
search |
Force search mode |
root |
Open the browser at Root() |
<ModuleName> |
Switch directly into a module (e.g. browser, cmd, math, terminal, bpm) |
<ModuleName>/seg1/seg2/... |
Open module with a route (passed to onRoute) |
#<ModuleName> |
Open the module in a tabbed elevated (full-workspace) UI |
ray <command> |
Headless: execute a Ray shorthand command without opening the UI |
debug |
Enable debug logging (combinable, e.g. debug root) |
Session state (last mode, last query) persists across invocations via GlobalVars.
- Compact – a floating overlay with a single-line search input, result list, and status bar. Default for keyboard-first workflows.
- Elevated – a full-screen tabbed workspace holding up to 5 concurrent module tabs, each with its own state. Enter via
#<ModuleName>or when opening a module markedelevatedOnly.
RayMA.lua Entry point, Main loop, auto-loaders
core/
state.lua Global state singleton, color cache, registries
helpers.lua Object introspection helpers (name/class/address/children)
ui.lua Compact UI builder and renderer
keys.lua Key dispatch and signal wiring
elevated.lua Tabbed full-workspace UI
modules/ Built-in modules (XML-registered)
photons/ Value parsers (color / beam / preset / fan)
prisms/ Symgrid layout algorithms (auto-loaded)
user_modules/ Drop-in user modules (auto-loaded, no XML edit)
Prisms, photons, and user modules are discovered at startup by scanning their directories (dir /B on Windows, ls on Unix). Files prefixed with _ load first so they can expose shared utilities.
| Module | Purpose |
|---|---|
search |
Fuzzy search over the data pools (Sequences, Groups, Macros, Presets, Pages, Layouts, Timecodes, Timers, Worlds, Plugins, Filters, MAtricks, Appearances). Type aliases (seq, grp, pre …) and 4 sort modes (relevance, children, type, datapool). |
browser |
Object tree browser — navigate Root, ShowData, data pools, and any MA3 object. |
properties |
Property inspector — all readable properties with type, value, and enum info. |
qa |
Quick Actions — context-sensitive menu (playback, edit, browse, cmdline, info). Extensible via RayMA.registerQAAction. |
cmd |
MA3 command line with visual caret. |
math |
Sandboxed calculator with history, math functions, pi/tau/phi/e, and ans. |
help |
Two-level help — module list → module details, auto-populated from module metadata. |
terminal |
Persistent shell terminal (LuaSocket TCP bridge, io.popen fallback). Windows cmd.exe / Unix bash / MA3 console. |
selection |
Live view of fixtures currently selected in the programmer. |
filebrowser |
Filesystem browser with copy/cut/paste, multi-select, sort, and filter. |
ray |
Shorthand command interpreter — dispatches to photon parsers and prism layouts; maintains history and result cache. |
Photons live in photons/ and are registered on load. Ray uses them to parse shorthand values.
- color —
#RRGGBB,#RGB,#RRGGBBAA, or named colors (red,blue, …). - beam — dimmer (
%), gobo (gobo N), strobe (strobe N), and position (pan/tilt/zoom). - preset — pool/preset by name or number, including
color 1.5-style references. - fan — distributes a value sequence across the selection (
red - blue - green), interpolating between stops.
Drop a new .lua file into photons/ to register a new parser.
Prisms live in prisms/ and implement fixture grid layout algorithms. Files starting with _ load first as shared utilities.
| Prism | Layout |
|---|---|
default |
Bird's-eye (2D top) if 3D data exists, else by type. |
2d |
Top/front/left/right projections with Z-band detection. |
auto |
Ring vs. linear auto-detection per type. |
compact |
Remove gaps while keeping structure. |
cylinder |
Cylindrical UV unwrap (angle → column, height → row). |
sphere |
Spherical UV unwrap (lat/lon). |
type |
One row per fixture type, mirror-aware columns. |
fid |
Group rows by FID prefix. |
flip, mirror |
Flip / mirror the current grid. |
_geometry, _grid_utils |
Shared geometry + grid helpers. |
Drop-in modules in user_modules/ are loaded automatically — no XML edit required. Files prefixed with _ (like _template.lua) are skipped by the loader and serve as references.
| Module | What it does |
|---|---|
banner |
ASCII banner display. |
bpm |
Tap-tempo BPM meter; applies to Speed master. |
catfacts |
Random cat facts. |
christmas |
Seasonal greeting. |
colorbucket |
Color mixing palette. |
colorslots |
3-reel slot machine that applies the resulting color (with strobe). |
countdown |
Configurable countdown timer. |
dmxdip |
DMX DIP-switch calculator (1–512). |
helloworld |
Minimal example module. |
luxcalc |
Lux calculator (aperture / shutter / ISO / ND). |
stats |
Show-file statistics — object counts and memory usage. |
subfixture |
Subfixture navigator grouped by fixture type. |
tcoffset |
Timecode offset calculator. |
timecodeeditor |
Timeline event editor with its own selection state. |
Drop a .lua file into user_modules/ — it's auto-loaded at startup, no XML edit. The module describes itself, draws a list of results into RayMA.T.RESULTS, and handles keys. Here's a complete, runnable example — user_modules/greet.lua:
-- user_modules/greet.lua — a tiny greeting picker
local S = RayMA.getModuleState("greet") -- state survives mode switches
S.greetings = S.greetings or { "Hello", "Hej", "Bonjour", "Ciao", "你好" }
local mod = {
name = "greet",
family = "Community",
description = "Pick a greeting and echo it",
helpEntries = {
{ "Type", "Filter greetings" },
{ "Enter", "Echo the selected greeting" },
{ "Esc", "Go back" },
},
}
local function rebuild(query)
local T, q = RayMA.T, (query or ""):lower()
T.RESULTS = {}
for i, g in ipairs(S.greetings) do
if q == "" or g:lower():find(q, 1, true) then
T.RESULTS[#T.RESULTS + 1] = { name = g, type = "Greeting", no = i }
end
end
end
function mod.onEnter()
RayMA.T.QUERY, RayMA.T.CARET, RayMA.T.CURSOR = "", 0, 1
rebuild("")
RayMA.UI.refresh()
end
function mod.renderRow(entry) return string.format(" #%-2d %s", entry.no, entry.name) end
function mod.getStatusText() return #RayMA.T.RESULTS .. " greetings | Enter: echo | Esc: back" end
function mod.onChar(c) RayMA.H.defaultOnChar(c); rebuild(RayMA.T.QUERY); RayMA.UI.refresh() end
function mod.onKey(keyCode)
local KC = RayMA.H.KC
if keyCode == KC.ESCAPE then RayMA.goBack(); return end
if RayMA.H.handleListKey(keyCode) then return end
if RayMA.H.handleTextKey(keyCode) then rebuild(RayMA.T.QUERY); RayMA.UI.refresh(); return end
if keyCode == KC.ENTER then
local e = RayMA.T.RESULTS[RayMA.T.CURSOR]
if e then Echo("[greet] " .. e.name) end
end
end
RayMA.registerModule(mod)
-- Optional: let users type "greet" to jump in, or launch with arg "greet"
RayMA.registerCommand({
name = "Greet", command = "greet", _module = "greet",
description = mod.description,
handler = function() RayMA.switchMode("greet") end,
})Key pieces:
RayMA.getModuleState(name)— persistent table, survivesonEnter/onExitcycles.RayMA.T— session state:QUERY,CARET,CURSOR,OFFSET,RESULTS(entries the UI renders).RayMA.H.defaultOnChar,handleListKey,handleTextKey,KC— input plumbing so you don't reimplement text editing and arrow-key navigation.RayMA.UI.refresh()— redraw after a state change.- Optional hooks:
onExit,onTick(called ~5×/s, useful for async work),onRoute(segments)(route-style args),onPluginClose,getSearchDisplay,onClick. - Set
elevatedOnly = trueon the module to force it to open in a tabbed workspace.
See user_modules/_template.lua for the full hook list and user_modules/helloworld.lua for a slightly more featured version.
Add a Quick Action:
RayMA.registerQAAction({ ... })Add a Ray command hint:
RayMA.registerRayHints({ ... })Prisms and photons are added by dropping a .lua file into the matching directory (no XML edit needed).
A prism implements a symgrid layout algorithm. It gets a list of fixtures with position + FID data, decides a row/column for each, and applies the resulting grid. Drop this into prisms/diagonal.lua:
-- prisms/diagonal.lua — place fixtures on the main diagonal
local P = RayMA.Prism
RayMA.registerPrism({
name = "diagonal",
description = "One fixture per row on the diagonal",
modes = { "diagonal" }, -- exposes "ray symgrid diagonal"
execute = function(fixtures, mode)
-- Assign each fixture its own row and column
local rows = {}
table.sort(fixtures, function(a, b) return a.fid < b.fid end)
for i, f in ipairs(fixtures) do
f.row = i - 1
f.col = i - 1
rows[i] = { f }
end
local layout = P.buildLayout(rows)
local ok = P.applyGrid(layout)
return ok, P.symResultMsg(rows, layout, "diagonal")
end,
})Fixture fields (populated by Ray before execute is called): fid, posX, posY, posZ, plus whatever you set on col / row.
Useful RayMA.Prism helpers: mapToGridIndices(fixtures, colKey, rowKey), centerAlignRows(rows), mirrorAssignCols(group), buildLayout(rows), applyGrid(layout), symResultMsg(rows, layout, label). See prisms/_grid_utils.lua and prisms/_geometry.lua for the full API.
Each entry in modes gets an auto-registered Ray hint (symgrid <mode>) so it shows up in the command palette.
A photon is a value parser + applier. It attaches helper functions to the shared RayMA.Photon namespace and registers Ray hints so users can discover it. Drop this into photons/kelvin.lua:
-- photons/kelvin.lua — apply a color temperature in Kelvin
local PH = RayMA.Photon
local function parseKelvin(str)
local k = str:match("^(%d+)[kK]$") -- matches "3200k", "5600K"
return k and tonumber(k) or nil
end
local function applyKelvin(k)
Cmd(string.format('Attribute "ColorRGB_R" At %d', k < 4000 and 100 or 80))
Cmd(string.format('Attribute "ColorRGB_G" At %d', 80))
Cmd(string.format('Attribute "ColorRGB_B" At %d', k < 4000 and 40 or 100))
return true, string.format("Colour temperature %dK", k)
end
-- Expose so other photons / the ray module can reuse them
PH.parseKelvin = parseKelvin
PH.applyKelvin = applyKelvin
-- Discoverability: show up in the Ray command list
RayMA.registerRayHints({
{ cmd = "3200k", desc = "Tungsten white", family = "Photon" },
{ cmd = "5600k", desc = "Daylight white", family = "Photon" },
})Photons don't have a dedicated registerPhoton call — they're just Lua files that publish functions onto RayMA.Photon and register Ray hints. The Ray module (modules/ray.lua) decides which photon handles a given input, so for deeper integration (e.g. making your parser participate in fan interpolation), wire it into the dispatch path in modules/ray.lua.
Files starting with _ (e.g. _geometry.lua) load first and are treated as shared utilities.
Launch with debug in the argument to enable RayMA.dbg(...) trace output to the MA3 command line. Combinable: debug root, debug search, etc.
No license file is currently shipped with this plugin.






