Spade

Level editor · Complete reference

Spade is the SPASTAZ level editor. Part 1 covers the editor itself; Part 2 is the reference for writing mods. Download Spade.

Part 1 — Using the Editor

The window

RegionContents
Menu barFile actions, Render, Help, Projects, Console, active project, snap toggles, grid readout. Wider than the window at this UI scale — hover it and scroll the wheel to reach the right-hand end. An arrow appears on the right edge while there is more to see.
Left columnTools, quick light placement, mod tool buttons. Scrolls with the wheel.
Viewports3D perspective plus Top (X/Z), Front (X/Y) and Side (Y/Z). The 2D views show live cursor coordinates in the bottom-left corner.
Right panelSeven tabs. Scrolls with the wheel; the scene tree is never truncated.
Status barLast message. Full history lives in the Console.

The editor 3D view is deliberately plain: flat key-light shading on a dark background, with no sky, shadows or ambient occlusion. It is a working view. Press Render to see the scene through the full lighting pipeline.

Tools

Select (Q)

Click objects in any viewport. In 2D views, the smallest brush under the cursor wins, so a small object inside a large one stays reachable.

KeyAction
G / R / TGrab, rotate, scale
X / Y / ZLock the transform to one axis
Enter or clickConfirm — Esc or right-click cancels
Ctrl+C / V / DCopy, paste, duplicate
Ctrl+L / Ctrl+IFlip horizontally / vertically
DeleteDelete the selection
H / Ctrl+HHide selection / unhide everything
CCentre every viewport on the selection

Block (B)

Drag a box in a 2D view, press Enter to create the solid. The shape row in the menu bar picks cube, cylinder, sphere, arc or plane.

Clip (C)

Drag a cut line in a 2D view. The plane is that line extruded along the view's depth axis. Tick marks show which side survives. Several cuts can be schemed up and applied together, and holding Ctrl while dragging curves the cut.

InputAction
DragDraw a straight cut
Ctrl + dragDraw a curved cut
Wheel (curve pending)Adjust the bulge
Shift + wheelAdjust how many segments approximate the arc (2–16)
TabCycle kept side: front, back, both
SpaceQueue this cut and start another (up to 8)
BackspaceRemove the last queued cut
EnterApply the whole queue
EscClear everything

What the kept side means

  • Front / Back — Each plane trims the solid. Chaining several gives their intersection — that is how you get chamfers, bevels and faceted shapes from one brush.
  • Both — Each plane also emits its offcut as a separate brush. The original is decomposed into convex pieces that tile it exactly.

Cutting an arched doorway

Select the wall, switch to Clip, set the kept side to Both with Tab, then Ctrl+drag an arc across it and tune the bulge with the wheel. Press Enter: the wall splits into the arch piece plus the surrounding pieces. Delete the piece filling the opening.

A curved cut is approximated by its segments, so an 8-segment arc becomes 8 planes. The queue holds up to 32 planes in total, which is where the 8-stroke and 16-segment limits come from.

Clipping cannot create the bent faces that free vertex editing can, so it is the safer way to shape geometry.

Vertex / Morph (V)

Click a solid to activate it; Ctrl+click adds more solids and edits them together.

KeyAction
1 / 2 / 3Vertex, Edge or Face sub-mode
Ctrl+clickAdd or remove a handle from the selection
Click same spot againCycle through handles stacked behind each other
Alt+click (2D)Grab every handle under the cursor at once
Drag empty spaceBox select; Enter or release commits
Ctrl+A / Ctrl+Shift+ASelect / clear all handles
Ctrl+BSnap handles to grid
Ctrl+MWeld coincident vertices
DeleteCollapse the selection to one point
Ctrl+FSplit a quad face along a diagonal (pick two opposite corners)
Ctrl+EEdge mode: insert a vertex at the edge midpoint
Ctrl+Shift+EFace mode: extrude along the normal
Ctrl+UFace mode: subdivide into quads
Shift+dragFace mode: slide along the face normal
BackspaceReset handles to clean box corners

Dropping a vertex onto another prompts to weld them — that is how a cube becomes a wedge or a pyramid. In a 2D view two corners often sit on the same pixel; a click takes the nearest, and clicking again steps to the one behind it.

Entity (E)

Place lights, props, spawns and physics objects. The kind row is in the menu bar. The left column also has one-click Point / Spot / Sun buttons that drop a light at the 3D view's focus, aimed where the camera is looking.

Texture (T)

Click a face in the 3D view to paint it. That is the whole workflow.

InputAction
ClickPaint the face with the active texture
Shift+clickPaint every face of that brush
Alt+clickSample the face — takes its texture and its alignment
Ctrl+clickPaint, and also apply the sampled alignment

Texturing

Every face carries its own texture, scale, offset, rotation, projection mode, flips and tint.

Projection modes

ModeBehaviourUse for
WorldUVs from world position. The texture is pinned to the world grid, so adjacent brushes line up automatically and sliding a brush slides it under the texture.Walls, floors, anything tiling across several brushes
LocalUVs from brush-local position. The texture is locked to the brush and travels with it — Hammer's texture lock.Props and detail pieces you will move around
FitUVs normalised to the face's own bounds: one tile covers the face exactly, whatever its shape. Scale becomes a tile count rather than world units.Signs, posters, decals

Controls (Obj tab, with a brush selected)

  • Scale U/V — world units per repeat, or tile count in Fit mode
  • Rot / R90 — degrees about the face normal; R90 steps in quarter turns
  • Off U/V — shift the texture across the surface
  • Flip U / Flip V — mirror either axis
  • Face R/G/B — per-face colour multiplier
  • Fit / World / Reset — one-click alignment
  • Copy / Paste Placement — transfer alignment between faces without changing which texture is on them

With faces selected in morph face-mode every control applies to just those faces; otherwise it applies to the whole brush.

Panels

TabContents
InfoCounts and undo depth
TreeEvery brush and entity. Scrolls — nothing is truncated.
ObjProperties of the selection. Every number has both a slider and a typeable box.
MdlModel assets with a live turntable preview
TexScrollable thumbnail grid with hover preview and dimensions
SetLighting quality, snapping, editor settings
ModsLoaded mods, per-tool status, and the last run's log

Every numeric field accepts typed input. Type a value and click away or press Enter; out-of-range entries are clamped rather than rejected.

Camera

Hold right mouse in the 3D view:

KeyAction
MouseLook
W A S DMove
Q / EStraight down / up on the world axis
Shift3× speed
WheelTrim fly speed

The view cannot roll — up is pinned to world up every frame — and pitch stops just short of vertical. In 2D views the wheel zooms and middle-drag pans.

Snapping

Four independent toggles in the menu bar: Mov, Rot, Scl, Vtx. Step sizes are in the Set tab; rotation defaults to 15°. The [ and ] keys halve and double the grid.

Projects

Projects are the main way in. The manager opens before the editor; if none exists one called Untitled is created.

projects/<name>/
  maps/       .mbsptz files; Save and Open default here
  exports/
  assets/
    models/
    textures/

Opening a project loads its most recently saved map automatically. Save and Open list only that project's maps, newest first. A filename containing a slash or a drive letter is treated as an absolute path, which is how you export elsewhere.

Autosave writes _autosave.mbsptz every two minutes when something has changed. If it is newer than every real save when you open the project, you are offered recovery. It is hidden from the Open dialog.

Console

Toggle from the menu bar. Captures both editor messages and raylib's own TraceLog output, so shader compile errors and driver warnings land here alongside your own actions. Filter by Editor / Info / Warn / Error, scroll with the wheel, Follow sticks to the newest line. Everything is also printed to stdout, so running from a terminal behaves normally.

If the render window looks wrong, check here first.

Mod tools

Mods add buttons under MODS in the left column. There are two kinds:

  • Command tools — Declared with tool= in the manifest. They fire once per click and return you to whatever you were doing. Each run is a single undo step.
  • Mode tools — Declared with mode=. Clicking one makes it the SELECTED tool, exactly like Select or Clip: it stays highlighted and its script runs every frame until you pick something else. These can read the keyboard and mouse, so they behave like built-in tools.

A mode tool asks for its own undo steps with mod.pushundo, because pushing one every frame would fill the history in seconds. It can also stand itself down with mod.exit.

Diagnostics

Alt+P runs Check For Problems: bent (non-planar) faces, degenerate faces, coincident vertices, orphaned vertices and empty brushes. Auto-Fix All welds duplicates and recomputes winding.

Bent faces come from free vertex editing. Fix them by splitting the face along a diagonal (Ctrl+F), or use the Clip tool instead.

File layout

<executable>/
  mods/                 global mods, one subfolder each
  projects/             all projects
  resources/
    shaders/            lighting.vs/.fs, depth.vs/.fs,
                        fullscreen.vs, ssao.fs, blur.fs
    icons/              light.png, light_spot.png, light_env.png,
                        physics.png, player_start.png, entity.png
    startup/            splash.png, splash.wav (both optional)
  assets/               fallback models/ and textures/

Shaders are #version 330. If they fail to compile the editor falls back to CPU shading and says so in the Set tab — check the Console for the compiler output.

Part 2 — Modding Reference

Where mods live

<folder containing spade_editor.exe>/
  mods/
    my_mod/
      mod.txt          <- required
      something.asm
      another.asm

Mods are global, not per project: one mods/ folder next to the executable, one subfolder per mod. Loose .asm files sitting directly in mods/ are ignored — the scanner only looks at subfolders, and only ones containing a mod.txt.

Mods are scanned at startup. Mods tab → Reload Mods rescans without restarting.

The manifest

name   = Example Pack
author = your name
 
# tool = fires once per click
tool = Pillar Row | pillars.asm | Place a row of 6 pillars
tool = Scatter    | scatter.asm | Scatter cubes around the selection
 
# mode = becomes the SELECTED tool and runs every frame
mode = Paint Cubes | paint.asm | Click in a 2D view to drop cubes

One key = value per line. # and ; start comments. CRLF and LF both work. Limits: 16 mods, 12 tools each, 2048 instructions and 64 variables per script.

tool versus mode

tool =mode =
Button behaves asPush buttonToggle, stays lit while selected
Script runsOnce per clickEvery frame while selected
UndoPushed automaticallyScript calls mod.pushundo when it edits
Typical useA command: scatter, taper, generateA tool: paint, drag, place with the mouse

Scripts assemble at load time, so a syntax error appears in the Mods tab as [!!] with a line number rather than when you click the button.

Language basics

A stack machine over 32-bit floats. One instruction per line, case-insensitive. Comments start with ; or #. Labels end with :.

PUSH 3          ; stack: 3
PUSH 4          ; stack: 3 4
ADD             ; stack: 7
STORE total     ; stack: empty, total = 7

There is no integer type; bitwise instructions truncate to int, operate, then convert back.

Operand order: the value pushed first is the left operand. PUSH 10 / PUSH 3 / SUB gives 7. The same holds for LT, DIV, POW and the rest.

Instruction set

Stack

OpBefore → AfterNotes
PUSH n→ nLiteral
POP / DROPa →Two spellings, one instruction
DUPa → a a
SWAPa b → b a
OVERa b → a b a
NIPa b → b
TUCKa b → b a b
ROTa b c → b c a
RROTa b c → c a bReverse rotate
DEPTH→ nCurrent stack size
CLEAR… →Empties the stack

Variables

OpEffect
LOAD namePush the variable
STORE namePop into the variable
INC namename += 1, touches no stack
DEC namename -= 1
ADDV namePop a value and add it to the variable

64 named slots, all starting at 0. INC is the idiomatic loop counter — shorter and faster than LOAD / PUSH 1 / ADD / STORE.

Binary arithmetic

OpResult
ADD SUB MUL DIVBasic arithmetic
MODFloat remainder, keeping the sign of the left operand
IDIVInteger division, truncated toward zero
POWa raised to b
MIN MAXSmaller / larger
ATAN2atan2(a, b)
HYPOTsqrt(a² + b²)
STEP1 if b >= a, else 0 (a is the edge)
WRAPa wrapped into [0, b). Always positive: -7 WRAP 3 is 2
SNAPTOa rounded to the nearest multiple of b
COPYSIGNMagnitude of a, sign of b

DIV, MOD and IDIV by zero abort with a line number rather than producing infinity. WRAP and SNAPTO by zero return a unchanged.

Unary arithmetic

OpResult
NEG ABS SIGNSign manipulation; SIGN gives -1, 0 or 1
FLOOR CEIL ROUND TRUNCRounding
FRACTFractional part, x - floor(x)
RECIP1/x, or 0 when x is about zero
SQR SQRTSquare and square root; SQRT of a negative gives 0
EXP LOG LOG2 LOG10Logs return 0 for non-positive input
SIN COS TANRadians
ASIN ACOSInput clamped to -1..1 first
ATANArc tangent
SINH COSH TANHHyperbolic
DEG RADConvert between radians and degrees
RANDPushes 0..1

RAND is seeded identically every run, so a mod produces the same result twice. That is deliberate: undo and re-run gives you the same thing.

Ternary

OpArgsResult
CLAMPv, lo, hiv limited to the range; lo and hi are swapped if reversed
LERPa, b, ta + (b - a) * t
SMOOTHSTEPedge0, edge1, xSmooth 0..1 ramp
BETWEENv, lo, hi1 if lo <= v <= hi

Comparison, logic and bitwise

GroupOpsNotes
ComparisonLT GT LE GE EQ NEPush 1 for true, 0 for false. EQ and NE use a 1e-6 tolerance, because exact float equality is a trap.
LogicNOT AND OR XOR ISZERO ISNANLogical, treating any non-zero as true
BitwiseBAND BOR BXOR BNOT SHL SHROperands truncate to int and convert back; shifts clamp to 0..31

Control flow

OpEffect
JMP labelJump
JZ labelPop; jump if zero
JNZ labelPop; jump if non-zero
JEQ JNE JLT JGT JLE JGEPop two; compare left against right; jump if true
GOSUB labelCall a subroutine, 32 levels deep
RETReturn from GOSUB
NOPNothing
HALTStop. End of file also stops.

The compare-and-jump family reads naturally as a loop test:

LOAD i
PUSH 10
JGE done        ; if i >= 10, jump

Output

OpEffect
PRINTPop and write to the run log
CALL logLog it but leave it on the stack

Output appears in Mods tab → Last Run and in the Console.

Host functions

CALL name. Arguments push left to right and pop automatically; every call pushes exactly one result, so POP it if unused. Out-of-range indices return -1 or 0 and do nothing. There are 225 of them.

Selection (11)

CallArgsDescription
sel.brush0selected brush index, or -1
sel.entity0selected entity index, or -1
sel.setbrush1(index) select a brush
sel.setentity1(index) select an entity
sel.clear0clear the selection
sel.tool0active tool 0..5
sel.settool1(tool) 0 sel 1 ent 2 blk 3 tex 4 vtx 5 clip
sel.texture0active texture index
sel.settexture1(index) set the active texture
sel.model0active model index
sel.setmodel1(index) set the active model

Map (6)

CallArgsDescription
map.brushcount0number of brushes
map.entcount0number of entities
map.grid0grid step
map.setgrid1(step) set the grid
map.clearbrushes0delete every brush
map.clearents0delete every entity

Camera (21)

CallArgsDescription
cam.posx03D camera X
cam.posy03D camera Y
cam.posz03D camera Z
cam.setpos3(x,y,z) move the camera, keeping its aim
cam.targetx0look-at point X
cam.targety0look-at point Y
cam.targetz0look-at point Z
cam.settarget3(x,y,z) aim at a point, keeping position
cam.fwdx0forward vector X
cam.fwdy0forward vector Y
cam.fwdz0forward vector Z
cam.yaw0yaw in degrees
cam.pitch0pitch in degrees
cam.setangles2(yaw,pitch) in degrees
cam.lookat3(x,y,z) point the camera at a spot
cam.fov0field of view
cam.setfov1(degrees) set field of view
cam.zoom02D viewport zoom
cam.setzoom1(zoom) set 2D viewport zoom
cam.orbit4(cx,cy,cz,radius) orbit a point at the current yaw
cam.dolly1(distance) move along the view direction

Snapping (10)

CallArgsDescription
snap.move01 if movement snapping is on
snap.rot01 if rotation snapping is on
snap.scale01 if scale snapping is on
snap.vertex01 if vertex snapping is on
snap.setmove1(0/1)
snap.setrot1(0/1)
snap.setscale1(0/1)
snap.setvertex1(0/1)
snap.rotstep0rotation step in degrees
snap.setrotstep1(degrees)

Settings (14)

CallArgsDescription
set.ambient0ambient light level
set.setambient1(0..1)
set.flyspeed0camera fly speed
set.setflyspeed1(speed)
set.gamma0render gamma
set.setgamma1(gamma)
set.specular0specular strength
set.setspecular1(0..1)
set.lighting01 if lighting is enabled
set.setlighting1(0/1)
set.gizmos01 if gizmo icons are shown
set.setgizmos1(0/1)
set.quality0render quality 0..3
set.setquality1(0 off 1 low 2 med 3 high)

Brush (56)

CallArgsDescription
brush.vertcount1(brush) vertex count
brush.facecount1(brush) face count
brush.getx2(brush,vert) local X
brush.gety2(brush,vert) local Y
brush.getz2(brush,vert) local Z
brush.setv5(brush,vert,x,y,z)
brush.posx1(brush) origin X
brush.posy1(brush) origin Y
brush.posz1(brush) origin Z
brush.setpos4(brush,x,y,z)
brush.sizex1(brush) nominal size X
brush.sizey1(brush) nominal size Y
brush.sizez1(brush) nominal size Z
brush.setsize4(brush,x,y,z) scales the mesh too
brush.rotx1(brush) pitch
brush.roty1(brush) yaw
brush.rotz1(brush) roll
brush.setrot4(brush,pitch,yaw,roll)
brush.add7(shape,x,y,z,sx,sy,sz) -> index
brush.delete1(brush) remove it
brush.clone1(brush) -> new index
brush.extrude3(brush,face,distance)
brush.splitedge3(brush,va,vb) -> new vertex
brush.weld2(brush,epsilon) -> removed
brush.flip2(brush,axis 0/1/2)
brush.subdivide2(brush,face) -> quads
brush.fixwind1(brush) recompute winding
brush.settex2(brush,textureIndex)
brush.gettex1(brush) texture index
brush.shape1(brush) shape it was built from
brush.rebuild2(brush,shape) regenerate as a primitive
brush.hide2(brush,0/1)
brush.ishidden1(brush) 1 if hidden
brush.physics1(brush) 1 if a physics object
brush.setphysics2(brush,0/1)
brush.mass1(brush) mass
brush.setmass2(brush,mass)
brush.friction1(brush) friction
brush.setfriction2(brush,friction)
brush.bounce1(brush) restitution
brush.setbounce2(brush,bounce)
brush.colr1(brush) colour red
brush.colg1(brush) colour green
brush.colb1(brush) colour blue
brush.setcol4(brush,r,g,b)
brush.minx1(brush) world bounds min X
brush.miny1(brush) world bounds min Y
brush.minz1(brush) world bounds min Z
brush.maxx1(brush) world bounds max X
brush.maxy1(brush) world bounds max Y
brush.maxz1(brush) world bounds max Z
brush.centrex1(brush) bounds centre X
brush.centrey1(brush) bounds centre Y
brush.centrez1(brush) bounds centre Z
brush.volume1(brush) signed mesh volume
brush.clip6(brush,nx,ny,nz,dist,keep 0=front 1=back 2=both)

Face (30)

CallArgsDescription
face.vertcount2(brush,face) corner count
face.vert3(brush,face,k) -> vertex index
face.nx2(brush,face) normal X
face.ny2(brush,face) normal Y
face.nz2(brush,face) normal Z
face.cx2(brush,face) centre X, local
face.cy2(brush,face) centre Y, local
face.cz2(brush,face) centre Z, local
face.area2(brush,face) surface area
face.tex2(brush,face) texture index, -1 = inherit
face.settex3(brush,face,texture)
face.scaleu2(brush,face) U scale
face.scalev2(brush,face) V scale
face.setscale4(brush,face,u,v)
face.offu2(brush,face) U offset
face.offv2(brush,face) V offset
face.setoff4(brush,face,u,v)
face.rot2(brush,face) texture rotation
face.setrot3(brush,face,degrees)
face.proj2(brush,face) 0 world 1 local 2 fit
face.setproj3(brush,face,mode)
face.flipu2(brush,face) 1 if U flipped
face.flipv2(brush,face) 1 if V flipped
face.setflip4(brush,face,flipU,flipV)
face.tintr2(brush,face) tint red
face.tintg2(brush,face) tint green
face.tintb2(brush,face) tint blue
face.settint5(brush,face,r,g,b)
face.split4(brush,face,va,vb) diagonal split
face.planarerr2(brush,face) how bent the face is

Entity (37)

CallArgsDescription
ent.add4(kind,x,y,z) -> index
ent.delete1(entity) remove it
ent.posx1(entity) X
ent.posy1(entity) Y
ent.posz1(entity) Z
ent.setpos4(entity,x,y,z)
ent.rotx1(entity) pitch
ent.roty1(entity) yaw
ent.rotz1(entity) roll
ent.setrot4(entity,pitch,yaw,roll)
ent.aim4(entity,dx,dy,dz) point it along a vector
ent.scale1(entity) scale
ent.setscale2(entity,scale)
ent.kind1(entity) kind 0..5
ent.setkind2(entity,kind)
ent.setlight5(entity,r,g,b,brightness)
ent.lightr1(entity) light red
ent.lightg1(entity) light green
ent.lightb1(entity) light blue
ent.bright1(entity) brightness
ent.range1(entity) light range
ent.setrange2(entity,range)
ent.inner1(entity) inner cone
ent.outer1(entity) outer cone
ent.setcone3(entity,inner,outer)
ent.casts1(entity) 1 if it casts light
ent.setcasts2(entity,0/1)
ent.mass1(entity) mass
ent.setmass2(entity,mass)
ent.physics1(entity) 1 if a physics object
ent.setphysics2(entity,0/1)
ent.hide2(entity,0/1)
ent.ishidden1(entity) 1 if hidden
ent.model1(entity) model index
ent.setmodel2(entity,model)
ent.tex1(entity) texture index
ent.settex2(entity,texture)

Assets (3)

CallArgsDescription
asset.texcount0loaded texture count
asset.modelcount0loaded model count
asset.iconcount0loaded icon count

System (7)

CallArgsDescription
sys.time0seconds since startup
sys.fps0current frames per second
sys.frametime0seconds for the last frame
sys.screenw0window width
sys.screenh0window height
sys.mousex0mouse X
sys.mousey0mouse Y

Keyboard (8)

CallArgsDescription
key.down1(code) 1 while the key is held
key.up1(code) 1 while the key is not held
key.pressed1(code) 1 on the frame it goes down
key.released1(code) 1 on the frame it comes up
key.shift01 while either shift is held
key.ctrl01 while either control is held
key.alt01 while either alt is held
key.any0keycode pressed this frame, or 0

Mouse (10)

CallArgsDescription
mouse.down1(button 0=L 1=R 2=M) 1 while held
mouse.pressed1(button) 1 on the frame it goes down
mouse.released1(button) 1 on the frame it comes up
mouse.wheel0wheel movement this frame
mouse.dx0mouse movement X this frame
mouse.dy0mouse movement Y this frame
mouse.vp0viewport under the cursor: 0 3D, 1 top, 2 front, 3 side, -1 none
mouse.worldx0cursor X in world space (2D views)
mouse.worldy0cursor Y in world space (2D views)
mouse.worldz0cursor Z in world space (2D views)

Mod Tool State (7)

CallArgsDescription
mod.ismode01 if this tool was declared with mode=
mod.active01 if this tool is the selected tool right now
mod.frame0frames since this tool became active
mod.exit0deselect this tool and return to Select
mod.get1(slot 0..31) read state that survives between frames
mod.set2(slot,value) write persistent state
mod.pushundo0record an undo step before the next edit

Utility (5)

CallArgsDescription
snap1(value) snapped to the grid
log1(value) log it, unchanged
dist36(x1,y1,z1,x2,y2,z2) distance
dot36(x1,y1,z1,x2,y2,z2) dot product
len33(x,y,z) vector length

Safety

Mods edit your map, so the interpreter is deliberately paranoid:

  • Step limit — 2,000,000 instructions. An infinite loop reports 'step limit reached' instead of hanging the editor.
  • Stack limit — 128 entries; underflow and overflow both abort with a line number.
  • Return stack — 32 levels; deeper GOSUB nesting aborts.
  • Bounds checks — Every brush, vertex, face, and entity index is checked against the live map.
  • Undo — Each run is wrapped in an undo push, so Ctrl+Z reverts the whole thing.
  • Division by zero — Aborts rather than producing infinity.

A failed run leaves the map as it was at the point of failure; it does not roll back automatically, but Ctrl+Z will.

Worked example

Taper a brush: pull every vertex above the midline toward the centre line.

CALL sel.brush
STORE b
 
LOAD b
PUSH 0
JLT done                  ; nothing selected
 
LOAD b
CALL brush.vertcount
STORE n
 
PUSH 0.45
STORE k
 
PUSH 0
STORE i
 
loop:
  LOAD i
  LOAD n
  JGE finish
 
  LOAD b
  LOAD i
  CALL brush.gety
  STORE vy
 
  LOAD vy
  PUSH 0
  JLE next                ; leave the bottom half alone
 
  LOAD b
  LOAD i
  CALL brush.getx
  LOAD k
  MUL
  STORE nx
 
  LOAD b
  LOAD i
  CALL brush.getz
  LOAD k
  MUL
  STORE nz
 
  LOAD b
  LOAD i
  LOAD nx
  LOAD vy
  LOAD nz
  CALL brush.setv
  POP
 
next:
  INC i
  JMP loop
 
finish:
  LOAD b
  CALL brush.fixwind
  POP
 
done:
  HALT

Call brush.fixwind after moving vertices by hand. Pulling a vertex through a face inverts it, and an inverted face renders inside-out.

Camera example

Frame the selected brush: aim the camera at its centre from a distance based on its size.

CALL sel.brush
STORE b
LOAD b
PUSH 0
JLT done
 
; centre of the brush
LOAD b
CALL brush.centrex
STORE cx
LOAD b
CALL brush.centrey
STORE cy
LOAD b
CALL brush.centrez
STORE cz
 
; radius from the bounding box diagonal
LOAD b
CALL brush.maxx
LOAD b
CALL brush.minx
SUB
STORE w
 
LOAD w
PUSH 2.5
MUL
PUSH 4
MAX                       ; never closer than 4 units
STORE dist
 
LOAD cx
LOAD cy
LOAD cz
LOAD dist
CALL cam.orbit
POP
 
done:
  HALT

Reading input

key.down, key.pressed and key.released take a raylib key code. Letters and digits are their ASCII values, so 'A' is 65 and '0' is 48. Common codes:

KeyCodeKeyCode
Space32Escape256
Enter257Tab258
Backspace259Delete261
Right arrow262Left arrow263
Down arrow264Up arrow265
A … Z65 … 900 … 948 … 57
Left shift340Left control341
Left alt342F1290

key.shift, key.ctrl and key.alt cover the modifiers without needing codes. Mouse buttons are 0 left, 1 right, 2 middle.

mouse.vp reports which viewport the cursor is over (0 3D, 1 top, 2 front, 3 side, -1 none), and mouse.worldx/y/z give the cursor position in world space for the 2D views — which is what you need to place anything under the pointer.

Mode tool example

A tool that drops a grid-snapped cube wherever you click in a 2D view, and exits on Escape. Declare it with mode= so it stays selected.

; exit when Escape is pressed
PUSH 256
CALL key.pressed
JZ carry_on
CALL mod.exit
POP
HALT
 
carry_on:
; only act on a fresh left click inside a 2D viewport
PUSH 0
CALL mouse.pressed
JZ done
 
CALL mouse.vp
STORE vp
LOAD vp
PUSH 1
JLT done                  ; -1 or 0 means not a 2D view
 
; snap the cursor position to the grid
CALL mouse.worldx
CALL snap
STORE px
CALL mouse.worldy
CALL snap
STORE py
CALL mouse.worldz
CALL snap
STORE pz
 
; one undo step per placement, not per frame
CALL mod.pushundo
POP
 
PUSH 0                    ; cube
LOAD px
LOAD py
LOAD pz
PUSH 2
PUSH 2
PUSH 2
CALL brush.add
POP
 
; count what we placed, in state that survives the frame
PUSH 0
CALL mod.get
PUSH 1
ADD
STORE total
PUSH 0
LOAD total
CALL mod.set
POP
LOAD total
CALL log
POP
 
done:
  HALT

mod.get and mod.set give each tool 32 float slots that persist between frames. Ordinary variables are cleared every run, so anything a mode tool needs to remember goes in a slot.

Idioms

Counted loop — there is no FOR.

PUSH 0
STORE i
loop:
  LOAD i
  PUSH 10
  JGE done
  ; body
  INC i
  JMP loop
done:

A subroutine. Put them after HALT so they only run when called.

PUSH 4
GOSUB cube_at_x
HALT
 
cube_at_x:
  STORE px
  PUSH 0
  LOAD px
  PUSH 0
  PUSH 0
  PUSH 2
  PUSH 2
  PUSH 2
  CALL brush.add
  POP
  RET

Discard results. Every CALL pushes something; ignore it inside a loop and the stack overflows.

Error messages

MessageCause
unknown instruction 'X'Typo, or no such instruction
unknown function 'X'Typo in a CALL name; they are case-sensitive
no such label 'X'Jump target never defined
stack underflowPopped more than pushed — usually a missing operand
stack overflowUsually a CALL result never POPped inside a loop
step limit reachedCounter never advances, or the exit test is inverted
divide by zeroGuard the divisor first
RET without GOSUBFell into a subroutine instead of jumping past it
GOSUB nested too deepRecursion deeper than 32 levels
missing script Xmod.txt names a file not in the folder