Lua scripting on the worksheet | Version 1.0
Available in RedcrabX from version 10.3.0
var, arr)setvar, setarr)calc, calcarr)
Lua manual: https://www.lua.org/manual/
The ProgramBox embeds a Lua 5.2 interpreter (MoonSharp) directly in the worksheet. It lets you write small programs with loops, conditions, and functions that can:
var.name.arr.name as a 1-based Lua table.calc() (scalar) and calcarr() (array).setvar() and setarr(), triggering automatic recalculation of all MathBoxes.| Property | Value | Description |
|---|---|---|
| Language | Lua 5.2 | Interpreted by MoonSharp – pure C#, no native DLL |
| Default size | 360 × 260 px | Resizable by dragging the box border |
| Default box name | P1 | Editable in the status toolbar name field |
| Run shortcut | Ctrl+Enter | Executes the script in the editor |
| Sandbox | SoftSandbox | File I/O and OS calls are blocked for safety |
The ProgramBox consists of three areas stacked vertically:
| Area | Description |
|---|---|
| Status toolbar |
Shows the box type label (Code), the editable name field (P1),
the ▶ Run button, and a status indicator
(Lua 5.2 → OK / Error).
|
| Code editor (top 60 %) | Monospaced editor (Consolas). Accepts Return and Tab. Horizontal and vertical scroll bars appear automatically. |
| Output view (bottom 40 %) |
Read-only. Shows everything written with print() and any runtime error messages.
Automatically scrolls to the last line after each run.
|
There are two ways to run the script:
Each run starts a fresh interpreter instance: variables from previous runs do not carry over. The output view is cleared and replaced with the new output.
io, os, and package are not
available. The safe standard library (math, string, table,
coroutine) is fully available.
Lua is a lightweight scripting language with a clean, readable syntax. Here is a brief cheat-sheet for users new to Lua:
-- Comments start with two dashes
-- Variables (all variables are global by default; use 'local' for local scope)
local x = 42
local name = "RedcrabX"
-- Arithmetic
local area = math.pi * 5^2 -- ^ is the power operator
-- String formatting
print(string.format("Area = %.4f", area))
-- Conditions
if x > 40 then
print("x is large")
elseif x == 40 then
print("exactly 40")
else
print("x is small")
end
-- Numeric for-loop (start, stop, step)
for i = 1, 10 do
io.write(i .. " ") -- io.write is available in SoftSandbox
end
for i = 0, 360, 30 do -- step = 30
print(math.sin(math.rad(i)))
end
-- While loop
local n = 1
while n <= 5 do
print(n)
n = n + 1
end
-- Functions
local function square(v)
return v * v
end
print(square(7)) --> 49
-- Tables (arrays use 1-based indexing)
local pts = {10, 20, 30, 40}
for i = 1, #pts do
print(pts[i])
end
math.sin, math.cos, math.sqrt, math.abs,
math.floor, math.ceil, math.exp, math.log,
math.pi, math.huge (infinity), etc.
Angles in Lua’s math library are always in radians —
use math.rad() to convert from degrees.
Scalar and array variables defined in MathBox cells on the same worksheet are available
in the ProgramBox as two read-only global tables:
var for scalars and arr for arrays.
Both are snapshots taken at the moment the script starts.
r := 50 // radius cx := 120 // center x cy := 100 // center y
local r = var.r -- reads the MathBox variable 'r'
local cx = var.cx
local cy = var.cy
print("radius =", r)
print(string.format("center = (%.0f, %.0f)", cx, cy))
var
holds scalars, arr holds arrays.
Matrix and string variables are not accessible at this time.
If a variable does not exist, var.name returns nil and
arr.name returns nil.
-- Use 'or' to supply a default when a variable may be undefined
local r = var.r or 40
print("r =", r)
arr
Array variables (defined with a range or list in MathBox) are accessible via
arr.name. Each value is a 1-based Lua table of numbers.
-- MathBox: data := [1, 4, 9, 16, 25]
local d = arr.data -- 1-based Lua table
print("length =", #d)
print("first =", d[1])
print("last =", d[#d])
-- Iterate over all elements
for i = 1, #arr.data do
print(i, arr.data[i])
end
arr.name to read an existing array variable directly.
Use calcarr("expr") (see section 8) to evaluate a MathBox range expression
such as [0..360:10] or a computed expression like "x^2".
The ProgramBox can write scalar and array values back to the shared variable store,
making them instantly visible to all MathBoxes on the worksheet. After any write via
setvar or setarr, all MathBoxes are recalculated automatically.
| Function | Signature | Description |
|---|---|---|
setvar |
setvar("name", value) | Write a scalar number to the worksheet variable store. |
setarr |
setarr("name", table) | Write a Lua table of numbers as an array variable. |
setvar("result", 42) -- create or overwrite scalar 'result'
setvar("I", var.U / var.R) -- derive from existing variables
setvar("area", math.pi * var.r^2) -- combine Lua math with worksheet variables
setarr("samples", {1, 2, 3, 4, 5}) -- write a literal array
-- Build a table dynamically and write it back
local t = {}
for i = 1, 10 do
t[i] = math.sin(math.rad(i * 36))
end
setarr("wave", t)
-- MathBox: n := 10
local n = var.n or 10
local a, b = 0, 1
local fibs = {}
for i = 1, n do
fibs[i] = a
a, b = b, a + b
end
setarr("fib", fibs) -- write array back to worksheet
setvar("fib_last", fibs[n]) -- write last value as scalar
print("fib_last =", fibs[n])
setvar / setarr
persist across worksheet refreshes and are re-applied automatically so MathBoxes that
reference them stay consistent. They are overwritten the next time the script runs.
calc and calcarr pass an expression string to the same engine
used by MathBox cells. All current worksheet variables (scalars and arrays) are in scope
inside the expression string.
| Function | Signature | Returns |
|---|---|---|
calc |
calc("expression") | number — the numeric result of the expression |
calcarr |
calcarr("expression") | 1-based Lua table of numbers |
-- MathBox variables r, m, c are in scope inside the expression string
local area = calc("pi * r^2")
local E = calc("m * c^2")
local ohm = calc("U / I")
print(string.format("area = %.4f", area))
-- Range generator [start..end:step]
local angles = calcarr("[0..360:10]") -- 37 elements: 0, 10, 20, ..., 360
for i = 1, #angles do
print(angles[i], math.sin(math.rad(angles[i])))
end
-- Read an existing array variable through the evaluator
local raw = calcarr("measurements") -- same result as arr.measurements
print("count =", #raw, " first =", raw[1])
calc and calcarr use the same
DEG / RAD setting as the MathBox (degrees by default).
calc("sin(90)") returns 1 in DEG mode.
arr.x for simply reading an existing array variable —
it is cleaner and skips the expression parser. Use calcarr for range
expressions such as [0..100:5] or computed expressions like "x^2".
Every function that works in a MathBox cell works identically inside a
calc() or calcarr() string — including all special
functions, aggregate functions, physics solvers, and electrical formulas.
This makes calc / calcarr a particularly powerful bridge:
Lua handles loops, conditions, and data structures while the MathBox engine provides
the full domain-specific function set.
calc() or calcarr().
| Category | Example functions |
|---|---|
| Standard math | sin, cos, log, sqrt, atan2, round, abs … |
| Special functions |
erf, erfc, gamma, lgamma, beta,
sigmoid, relu, softplus,
besselj, bessely, airy_ai, airy_bi
|
| Array & aggregate |
sum, min, max, count, median,
stddev, diff, aver, clip,
fill, new, product
|
| Physics |
pfa, centforce, kine, poten,
freefall, orbvel, work, slopef …
|
| Electrical |
voltdrop, rccharge, parres, rlseries,
lcrres, pow_factor …
|
| Geometry | p2dist, p2linedist … |
-- Gamma function: Γ(5) = 4! = 24
print(calc("gamma(5)")) -- 24
-- Error function at a fixed point
print(calc("erf(1)")) -- ≈ 0.84270
-- sigmoid applied element-wise via calcarr
local xs = calcarr("[−3..3:0.5]")
local sig = calcarr("sigmoid([−3..3:0.5])")
for i = 1, #xs do
print(string.format("x=%5.1f sigmoid=%.5f", xs[i], sig[i]))
end
-- Bessel function J₀ profile, written back as an array variable
local J0 = calcarr("besselj(0, [0..12:0.25])")
setarr("J0_profile", J0)
print(string.format("%d Bessel values written", #J0))
-- MathBox: measurements := [3.1, 2.9, 3.4, 3.0, 2.8]
local n = calc("count(measurements)")
local mean = calc("sum(measurements) / count(measurements)")
local spread = calc("stddev(measurements)")
print(string.format("n=%d mean=%.3f σ=%.3f", n, mean, spread))
-- First differences (delta between successive samples)
local deltas = calcarr("diff(measurements)")
for i = 1, #deltas do
print(string.format("Δ[%d] = %.4f", i, deltas[i]))
end
-- Physics: same named-parameter syntax as in a MathBox cell
local tf = calc("freefall(h=80)") -- free-fall time from 80 m [s]
local Ek = calc("kine(m=2, v=10)") -- kinetic energy ½mv² [J]
print(string.format("tf = %.3f s, Ek = %.1f J", tf, Ek))
-- Electrical: parallel resistance of two worksheet variables R1, R2
local Rp = calc("parres(R1, R2)")
print(string.format("R_par = %.4f Ω", Rp))
-- RC voltage drop over a time range, stored as array
local t_vec = calcarr("[0..0.1:0.005]")
local u_vec = calcarr("rccharge(U=230, R=470, C=0.001, t=[0..0.1:0.005])")
setarr("rc_voltage", u_vec)
print(string.format("u(t=0.1 s) ≈ %.2f V", u_vec[#u_vec]))
FUNCTIONS.md in the project root and
the individual guide pages (Physics,
Electrics, Geometry)
for complete signatures of all supported MathBox functions.
All DrawBox draw functions are available as global Lua functions
inside the ProgramBox. First select the target DrawBox with drawtarget, then call
any draw function. The coordinate system is the same as in MathBox draw commands:
origin at top-left (0, 0), X increases rightward, Y increases downward.
-- Select DrawBox D1 as the drawing target
drawtarget("D1") -- note: string argument required in Lua
-- Draw a background grid
drawgrid(20)
-- Draw shapes using a loop
for i = 0, 11 do
local angle = math.rad(i * 30)
local x = 100 + 60 * math.cos(angle)
local y = 100 + 60 * math.sin(angle)
drawdot(x, y, 4)
end
drawtext(82, 10, "Clock positions")
drawtarget(D1)). In Lua you must pass it as a string:
drawtarget("D1").
drawclear() at the start of the script to erase shapes
left from a previous run before drawing new ones.
All functions below operate on the current DrawBox target set with drawtarget("name").
Coordinates are in pixels; the origin is the top-left corner of the canvas.
| Function | Parameters | Description |
|---|---|---|
drawtarget(name) | name: string | Select the active DrawBox by name, e.g. "D1" |
drawclear() | — | Clear all shapes from the active DrawBox |
drawgrid(spacing) | spacing: number | Draw a light gray background grid; call before other draw commands |
drawgrid(sx, sy) | sx, sy: numbers | Grid with different horizontal / vertical spacing |
drawrect(x, y, h, w) | x, y, h, w: numbers | Draw a rectangle; (x,y) = top-left corner, h = height, w = width |
drawcircle(x, y, r) | x, y, r: numbers | Draw a circle at center (x,y) with radius r |
drawellipse(x, y, rx, ry) | x, y, rx, ry: numbers | Draw an ellipse at center (x,y) with half-axes rx and ry |
drawarc(x, y, r, a1, a2) | x, y, r, a1, a2: numbers | Draw a circular arc; angles in degrees, clockwise from east |
drawline(x1, y1, x2, y2) | numbers | Draw a line segment from (x1,y1) to (x2,y2) |
drawarrow(x1, y1, x2, y2) | numbers | Draw a line with a filled arrowhead at (x2,y2) |
drawdot(x, y, r) | x, y, r: numbers | Draw a filled black disk at (x,y) with radius r |
drawtext(x, y, text) | x, y: numbers; text: string | Draw a text label; (x,y) is the top-left corner |
drawcross(x, y, size) | x, y, size: numbers | Draw a crosshair marker at (x,y); size = total arm length |
drawdim(x1,y1,x2,y2,offset) | numbers | Draw a full dimension line with extension lines and arrowheads |
drawpoly({x1,y1,x2,y2,...}) | Lua table of numbers | Draw a closed polygon from coordinate pairs (at least 3 pairs) |
math.deg() to convert
a radian value to degrees before passing it to drawarc.
print("Hello from ProgramBox!")
print("2 + 2 =", 2 + 2)
print(string.format("pi = %.6f", math.pi))
Output:
Hello from ProgramBox! 2 + 2 = 4 pi = 3.141593
print(string.format("%-8s %10s %10s", "deg", "sin", "cos"))
print(string.rep("-", 30))
for deg = 0, 360, 30 do
local r = math.rad(deg)
print(string.format("%-8.0f %10.4f %10.4f", deg, math.sin(r), math.cos(r)))
end
-- MathBox: voltage := 230 resistance := 47
local U = var.voltage or 230
local R = var.resistance or 100
local I = U / R
local P = U * I
print(string.format("U = %.1f V", U))
print(string.format("R = %.1f Ω", R))
print(string.format("I = %.4f A", I))
print(string.format("P = %.2f W", P))
drawtarget("D1")
drawclear()
drawgrid(20)
local cx, cy, r = 120, 120, 80
-- Outer circle
drawcircle(cx, cy, r)
drawdot(cx, cy, 3)
-- Hour marks
for h = 0, 11 do
local angle = math.rad(h * 30 - 90)
local x1 = cx + (r - 8) * math.cos(angle)
local y1 = cy + (r - 8) * math.sin(angle)
local x2 = cx + r * math.cos(angle)
local y2 = cy + r * math.sin(angle)
drawline(x1, y1, x2, y2)
end
-- Hour labels
local labels = {"12","1","2","3","4","5","6","7","8","9","10","11"}
for h = 0, 11 do
local angle = math.rad(h * 30 - 90)
local x = cx + (r - 20) * math.cos(angle) - 4
local y = cy + (r - 20) * math.sin(angle) - 7
drawtext(x, y, labels[h + 1])
end
-- Hands (read from MathBox variables or use defaults)
local hour_angle = math.rad((var.hh or 10) * 30 + (var.mm or 10) * 0.5 - 90)
local minute_angle = math.rad((var.mm or 10) * 6 - 90)
drawarrow(cx, cy, cx + 0.55 * r * math.cos(hour_angle),
cy + 0.55 * r * math.sin(hour_angle))
drawarrow(cx, cy, cx + 0.80 * r * math.cos(minute_angle),
cy + 0.80 * r * math.sin(minute_angle))
-- MathBox: sides := 6 radius := 60
local n = var.sides or 6
local r = var.radius or 60
local cx = 120
local cy = 100
drawtarget("D1")
drawclear()
local pts = {}
for i = 0, n - 1 do
local angle = math.rad(i * 360 / n - 90)
pts[#pts + 1] = cx + r * math.cos(angle)
pts[#pts + 1] = cy + r * math.sin(angle)
end
drawpoly(pts)
drawtext(cx - 10, cy - 8, n .. "-gon")
-- MathBox: w := 120 h := 80
local W = var.w or 120
local H = var.h or 80
local ox, oy = 40, 40 -- origin of the rectangle
drawtarget("D1")
drawclear()
drawgrid(10)
drawrect(ox, oy, H, W)
drawcross(ox, oy, 12)
drawcross(ox + W, oy + H, 12)
-- Width dimension below
drawdim(ox, oy + H, ox + W, oy + H, 20)
drawtext(ox + W/2 - 15, oy + H + 26, W .. " px")
-- Height dimension to the right
drawdim(ox + W, oy, ox + W, oy + H, 20)
drawtext(ox + W + 28, oy + H/2 - 7, H .. " px")
-- MathBox: base := 3 height := 5
local b = var.base or 3
local h = var.height or 5
local area = 0.5 * b * h
setvar("triangle_area", area) -- MathBoxes that use 'triangle_area' now update
print(string.format("Area = %.4f", area))
-- MathBox: data := [1, 4, 9, 16, 25]
local src = arr.data
if src == nil then print("no 'data' array found") ; return end
local cumsum = {}
local s = 0
for i = 1, #src do
s = s + src[i]
cumsum[i] = s
end
setarr("cumsum", cumsum) -- write cumulative sums back to worksheet
print("total sum =", s)
-- MathBox: f := 1000 L := 0.01
local Xc = calc("1 / (2 * pi * f * L)")
local Xl = calc("2 * pi * f * L")
print(string.format("Xc = %.4f Ω", Xc))
print(string.format("Xl = %.4f Ω", Xl))
-- Build a time vector and a sine wave from it
local t_vec = calcarr("[0..1:0.05]") -- 0.00, 0.05, 0.10, ..., 1.00
local wave = {}
for i = 1, #t_vec do
wave[i] = math.sin(2 * math.pi * t_vec[i])
end
setarr("sine_wave", wave)
print(string.format("%d samples written to 'sine_wave'", #wave))
| Message (output view) | Cause | Fix |
|---|---|---|
Syntax error: … |
The Lua code contains a syntax mistake. | Check the line number in the message and correct the Lua syntax. |
Runtime error: … |
An error occurred during script execution (e.g. calling nil, division by zero). | Add nil-checks with or defaults; verify variable names. |
drawpoly: argument must be a table |
drawpoly received something other than a Lua table. |
Pass a table of numbers: drawpoly({x1,y1,x2,y2,...}). |
drawpoly: element [n] must be a number |
A non-numeric value appeared in the coordinate table. | Ensure all entries in the table are numbers. |
| Status: Error (red) | Any error that prevented the script from completing. | Read the full error text in the output view. |
setvar: expected setvar("name", number). |
First argument is not a string, or fewer than 2 arguments were passed. | Use setvar("varname", value) with a quoted name and a numeric value. |
setvar: argument 2 is a table; use setarr(…) |
A Lua table was passed as the value to setvar. |
Use setarr("name", {…}) for array values. |
setvar: argument 2 must be a number. |
Second argument cannot be converted to a number (e.g. a string or boolean). | Pass a numeric expression: setvar("x", 42) or setvar("x", var.a + 1). |
setarr: expected setarr("name", table). |
First argument is not a string, or fewer than 2 arguments were passed. | Use setarr("varname", table) with a quoted name. |
setarr: argument 2 must be a table, e.g. {1, 2, 3}. |
Second argument to setarr is not a Lua table. |
Pass a table literal {1,2,3} or a local table variable. |
setarr: all table values must be numbers. |
A non-numeric entry was found in the table passed to setarr. |
Ensure every element of the table is a number before calling setarr. |
calc: expected calc("expression"). |
calc was called without arguments or with a non-string argument. |
Pass an expression string: calc("pi * r^2"). |
calc: <message> |
The MathBox expression evaluator rejected the expression. | Check variable names and expression syntax; the message includes the evaluator’s own error text. |
calcarr: expected calcarr("expression"). |
calcarr was called without arguments or with a non-string argument. |
Pass an expression string: calcarr("[0..360:10]"). |
calcarr: cannot evaluate "…". |
The expression could not be evaluated as an array by the MathBox engine. | Use a range expression like [start..end:step] or an existing array variable name. |
| Status: OK (green) | — | Script completed without errors. |
drawarc always uses degrees (clockwise from east), independent of the MathBox
DEG/RAD setting. All other Lua math functions (math.sin, math.cos,
etc.) use radians. Use math.rad() and math.deg() to convert.
drawclear() at the top of your script if you
want a clean canvas on each run.
var.name returns nil;
if an array variable is not defined, arr.name returns nil.
Use var.name or default to supply a fallback value and guard array access
with if arr.name == nil then … end.
P1 by default) is saved with the workspace. It is not yet used
as a target for other boxes, but serves as a label for identification.
io.open, os.execute, and require.
Standard output via print() is fully available.
var.x or arr.x is purely passive and never changes the
worksheet. Only setvar and setarr write back and cause MathBoxes
to update.
setvar("x", {1,2,3}) raises a runtime error. Use setarr for
array values and setvar only for single numbers.
RedcrabX ProgramBox Guide — further topics: Quick Start Guide | DrawBox Guide | MathBox Guide | Chart Guide | Plot Guide