Editor Scripting API

The editor scripting API is for editor tools – scripts under an editor/ folder of your project. Tools run alongside your game script and talk to the editor itself: read and change the current selection, query and drive the editor camera, group several changes into a single undo step, and put their own widgets next to the editor’s. require engine.editor brings the whole API in.

Two annotations attach a function to a component’s inspector section. The function takes the component it operates on as its single argument – the argument’s type picks the section, and comp.nodeId is the node the component belongs to:

  • [menu_item] def my_tool(comp : Mesh?) – an entry in that component’s context menu;

  • [tool_button] def my_tool(comp : Mesh?) – a button above the component’s fields (below – below them, new_line – start a new button row).

For a plain button that is not tied to a component, use the tool_button widget function (see the widgets below).

Two more annotations hook a function into the editor loop (the function name is free):

  • [editor_update] def f() – runs every editor frame; the usual place to declare tool UI widgets (see below);

  • [editor_node_changed] def f(node : NodeId) – runs when the editor changed a node: an inspector edit, a gizmo drag, an undo or a redo.

Whether the game is running: game_state() returns Stopped, Paused or Playing.

Every change made through this API is registered in the editor’s undo history, exactly like an edit made by hand in the editor UI.

The prefab currently open in the editor is reachable too: prefab_name() returns its file name, prefab_save() saves it like Ctrl+S, and prefab_open("prefabs/level1.prefab") queues switching to another prefab by its project-relative path.

Tools draw their own UI without any window management. Inside an [editor_update] hook, every tool_* call (tool_slider, tool_button, tool_label, …) declares a widget for this frame and returns its current value; keep calling it every frame to keep the widget on screen. When the call stops running the widget disappears after about a second, but its value is kept and comes back with the widget. Values of input widgets (sliders, float3, color, checkbox, text input, enum combo, resource picker) also persist in the project settings, surviving an editor restart – in .project/settings.blk next to the project sources, so they are shared with the team. Per-node values of component_tool widgets persist too, keyed by the node:

[editor_update]
def scatter_ui() {
    tool_window("Scatter") {
        let radius = tool_slider("radius", 5., 0.1, 50.)
        tool_hint("Scatter radius in meters")
        if (tool_button("scatter")) {
            scatter(get_selected_nodes(), radius)
        }
    }
}

The widgets live in floating tool windows over the viewport, so the viewport itself never shrinks or jumps:

  • the default Tools overlay – translucent, draggable, collapsible. Widgets declared outside any tool_window block land here; it cannot be created explicitly;

  • a normal window per tool_window("name") { ... } block – draggable, resizable, closable, with the widgets declared inside the block. Blocks nest, and tool_window(name, bg_alpha) { ... } overrides any window’s background opacity (including the default overlay via "");

  • a component_tool("Mesh") { ... } block draws its widgets in that component’s inspector section instead, per inspected node.

All tool windows are listed in the editor’s top Window menu, where a closed one can be reopened.

A widget is identified by its name and its call site. The name is the visible label and pins the tuned value across code edits; the call site tells apart unnamed widgets (labels, separators, line breaks) and repeated declarations – one call site hit N times in a loop yields N widgets, in order. Widgets draw in declaration order.

Enumerations

EditorTab

The editor’s main view tabs.

Values:
  • Welcome = 0 - The start page

  • Scene = 1 - The game scene view

  • Game = 2 - The running game view

  • Prefab = 3 - The prefab editor

GameState

The game as the editor’s Play/Pause buttons see it; returned by game_state(). A stopped game with a leftover pause flag reads as Stopped.

Values:
  • Stopped = 0 - The game is not started

  • Paused = 1 - The game is started but frozen - manually, by compilation errors or by the user menu (holds in the prefab tab too)

  • Playing = 2 - The game is started and running

GizmoOperation

The viewport gizmo mode, as picked by the toolbar buttons or the W/E/R/T/Y hotkeys.

Values:
  • Translate = 0 - Move handles (W)

  • Rotate = 1 - Rotate handles (E)

  • Scale = 2 - Scale handles (R)

  • Bounds = 3 - The 2D rect tool (T)

  • Universal = 4 - Translate, rotate and scale handles at once - the Transform toolbar button (Y)

ShadeMode

The viewport shading mode from the editor’s Options menu.

Values:
  • Shaded = 0 - Normal shading

  • Wireframe = 1 - Wireframe only

  • ShadedWireframe = 2 - Wireframe drawn over the shaded view

Selection

clear_selection()

Clears the editor selection. Same as set_selection with an empty array (undoable).

get_first_selected_node(): NodeId

Returns the first selected node, or an invalid NodeId when nothing is selected or no editor is running.

get_selected_nodes(): array<NodeId>

Returns a copy of the currently selected nodes, in selection order. Empty when nothing is selected or no editor is running.

set_selection(nodes: array<NodeId>)

Replaces the current editor selection with nodes (empty array = clear selection). Registered as an undoable action; inside a batch_actions() block it merges into the batch’s single undo step.

Arguments:

Camera

enter_camera_2d_mode()

Switches the editor camera of the active view into 2D (UI) mode, same as clicking the “UI” button in the camera toolbar: it stores the current 3D camera state and smoothly frames the scene’s UI canvases in orthographic projection. No-op when no editor is running.

exit_camera_2d_mode()

Switches the editor camera of the active view back to its last 3D state, same as clicking the “UI” button again. No-op when no editor is running.

get_camera_forward(): float3

Returns the world forward vector of the editor camera.

get_camera_fov(): float

Returns the editor camera’s field of view in degrees (perspective mode). Returns 0 when no editor is running.

get_camera_orthographic_scale(): float

Returns the editor camera’s orthographic zoom (half-height of the view volume, in world units). Returns 0 when no editor is running.

get_camera_right(): float3

Returns the world right vector of the editor camera.

get_camera_transform(): float3x4

Returns the world transform of the editor camera of the active view (columns: right, up, forward, position). Identity when no editor is running.

get_camera_up(): float3

Returns the world up vector of the editor camera.

get_camera_zfar(): float

Returns the editor camera’s far clip distance. Returns 0 when no editor is running.

get_camera_znear(): float

Returns the editor camera’s near clip distance. Returns 0 when no editor is running.

is_camera_2d(): bool

Returns true if the editor camera of the active view is in 2D (UI) mode. Returns false when no editor is running.

is_camera_orthographic(): bool

Returns true if the editor camera of the active view is in orthographic projection mode. Returns false when no editor is running.

look_at(center: float3; radius: float)

Smoothly frames the sphere at center with the given radius, keeping the camera orientation.

Arguments:
  • center : float3

  • radius : float

look_at(node: NodeId)

Smoothly frames node in the editor viewport, exactly like pressing F on a selected node: the camera keeps its orientation and moves back until the node’s bounds fit the view.

Arguments:
look_at_instant(center: float3; radius: float)

Same as look_at, but the camera jumps immediately.

Arguments:
  • center : float3

  • radius : float

look_at_instant(node: NodeId)

Same as look_at, but the camera jumps immediately - a tool reading the camera right after the call sees the final state.

Arguments:
set_camera_clip(znear: float; zfar: float)

Sets the editor camera’s near and far clip distances instantly (validated the same way as the camera settings popup: znear is clamped to a minimum, and zfar falls back to its maximum if it would end up too close to or below znear). No-op when no editor is running.

Arguments:
  • znear : float

  • zfar : float

set_camera_fov(deg: float)

Sets the editor camera’s field of view in degrees, instantly (clamped to the same range as the camera settings popup). No-op when no editor is running.

Arguments:
  • deg : float

set_camera_orthographic()

Switches the editor camera of the active view to orthographic projection, instantly. No-op when no editor is running.

set_camera_orthographic_scale(scale: float)

Sets the editor camera’s orthographic zoom instantly (clamped to the same range as the camera settings popup). Takes effect immediately, whether or not the camera is currently in orthographic mode. No-op when no editor is running.

Arguments:
  • scale : float

set_camera_perspective()

Switches the editor camera of the active view to perspective projection, instantly. No-op when no editor is running.

set_camera_position(pos: float3)

Moves the editor camera to pos instantly. A tool reading the camera right after the call sees the new state.

Arguments:
  • pos : float3

set_camera_rotation(rot: quat4)

Rotates the editor camera to rot instantly. The camera pitch is clamped to +-90 degrees, same as when rotating it with the mouse.

Arguments:
set_camera_transform(tm: float3x4)

Sets the editor camera position and rotation from a world transform instantly. The scale part is ignored.

Arguments:

Editor actions

batch_actions(cb: block<():void>)

Groups editor actions that are each ALREADY undoable on their own (set_selection, snap_to_grid, snap_to_surface, …) into a single undo step: one Ctrl+Z reverts the whole block instead of one action at a time. It does not make anything undoable by itself – raw game-API scene mutations (create_node, set_parent, component writes) register no actions and are NOT captured; wrap those in undoable instead.

Rule of thumb: everything inside is already undoable one-by-one -> batch_actions; the block mutates the scene through the game API -> undoable. batch_actions is free (two markers cross into the editor, no snapshots):

batch_actions() {
    for (node in get_selected_nodes()) {
        snap_to_surface(node)
    }
}
Arguments:
  • cb : block<void>

snap_to_grid(node: NodeId): bool

Snaps the node’s world position to the translation grid step from the editor preferences: each axis rounds to the nearest multiple of the step. A step of 0 (grid disabled) leaves the position untouched. Undoable. Returns false when the node is dead:

snap_to_grid(node)
Arguments:
snap_to_surface(node: NodeId): bool

Drops the node straight down onto the first surface below it - other nodes’ geometry; the node itself and its children are ignored. Undoable. Returns false when there is nothing below the node:

if (!snap_to_surface(node)) {
    print("nothing under the node")
}
Arguments:
undoable(name: string; blk: block<():void>)

Makes every scene mutation done inside the block - create_node, set_parent, component and field changes, anything from the game API - ONE undoable editor step. Game-API mutations are not undoable on their own; this is what makes them so: the open prefab is snapshotted before and after the block, and undo/redo restore the snapshots, so nodes created inside the block get their ids back on redo. Needs an open prefab. A block that changes nothing registers nothing.

Rule of thumb: the block mutates the scene through the game API -> undoable; everything inside is already undoable one-by-one (set_selection, snap_to_grid, …) -> batch_actions. The two snapshots serialize the whole prefab, which is not free on a big one - so do not wrap a couple of already-undoable editor actions in undoable, batch_actions does that at no cost:

undoable("scatter") {
    for (i in range(10)) {
        let node = create_node(NodeData(name = "tree_{i}", parent = parent))
        snap_to_surface(node)
    }
}
Arguments:
  • name : string

  • blk : block<void>

Tool UI widgets

component_tool(component: string; blk: block<():void>)

Shows the tool widgets declared inside the block in component’s inspector section, next to the component’s own fields, for the node being inspected. The widgets are per-node: values tuned for one node do not leak into another’s section. component_tool_node() returns the node the section belongs to:

component_tool("Mesh") {
    let intensity = tool_slider("intensity", 1., 0., 2.)
    if (tool_button("rebuild")) {
        rebuild_mesh(component_tool_node(), intensity)
    }
}
Arguments:
  • component : string

  • blk : block<void>

component_tool_node(): NodeId

The node the widgets of a component_tool block are shown for: the one currently open in the editor’s inspector. Stays at the last inspected node when the inspector is empty; invalid NodeId until anything was inspected - check with valid() before acting on it.

tool_button(name: string; icon: string = ""; selected: bool = false): bool

Declares a button in the current tool window; returns true once per click. icon is an svg drawn on the button next to the name; start the name with ## for an icon-only button (the name stays the widget’s id). selected draws the button in the pressed style - together with icon-only buttons in a loop that is a tileset picker:

for (i in range(length(tiles))) {
    if (tool_button("##tile{i}", tiles[i].icon, g_currentTile == i)) {
        g_currentTile = i
    }
}
Arguments:
  • name : string

  • icon : string

  • selected : bool

tool_checkbox(name: string; def_value: bool): bool

Declares a checkbox in the current tool window and returns its current state:

let mirror = tool_checkbox("mirror", false)
Arguments:
  • name : string

  • def_value : bool

tool_color(name: string; def_value: float3): float3

Declares an RGB color field in the current tool window and returns its current value (0..1 per channel). Clicking the color swatch opens the full picker:

let tint = tool_color("tint", float3(1, 0.5, 0.2))
Arguments:
  • name : string

  • def_value : float3

tool_combo(name: string; def_value: auto(TT)): TT

Declares a dropdown over all values of an enum type in the current tool window and returns the chosen value; def_value picks both the enum and the initial selection. The choice persists in the project settings by value name:

enum ScatterMode {
    Grid
    Random
    Poisson
}
let mode = tool_combo("mode", ScatterMode.Random)
Arguments:
  • name : string

  • def_value : auto(TT)

tool_float3(name: string; def_value: float3; speed: float = 0.1f): float3

Declares a float3 drag field (x/y/z, drag any of them to change; speed = value change per pixel dragged) in the current tool window and returns its current value:

let offset = tool_float3("offset", float3(0, 1, 0))
Arguments:
  • name : string

  • def_value : float3

  • speed : float

tool_hint(text: string)

Attaches a hover tooltip to the widget declared just before this call (any tool_* widget, window or component scope):

let radius = tool_slider("radius", 5., 0.1, 50.)
tool_hint("Scatter radius in meters")
Arguments:
  • text : string

tool_input(name: string; def_value: string): string

Declares a text input in the current tool window and returns its current text:

let prefix = tool_input("prefix", "scattered_")
Arguments:
  • name : string

  • def_value : string

tool_label(name: string; text: string)

Shows a line of changing text in the current tool window: the widget is keyed by name, so text may be different every frame. Use it for dynamic readouts (the single-argument overload is for fixed captions):

tool_label("state", "selected={length(get_selected_nodes())}")
Arguments:
  • name : string

  • text : string

tool_label(text: string)

Shows a line of fixed text in the current tool window:

tool_label("Scatter tool")
Arguments:
  • text : string

tool_new_line()

Breaks the current widget row: the following widgets start on a new line.

tool_progress(name: string; fraction: float)

Shows a progress bar in the current tool window. fraction (0..1) is supplied by the tool each frame - the value flows from the code, not from the UI:

tool_progress("baking", float(done) / float(total))
Arguments:
  • name : string

  • fraction : float

tool_resource(name: string; type_: auto(TT)): TT

Declares a resource picker in the current tool window: a field showing the chosen resource plus a Select button opening the editor’s resource popup, filtered to the requested type. The type<…> argument picks both the filter and the return type. The choice persists across sessions (stored as the asset’s guid):

let mesh = tool_resource("mesh", type<MeshId>)
let decal = tool_resource("decal", type<MaterialId>)
Arguments:
  • name : string

  • type_ : auto(TT)

tool_separator()

Inserts a separator between the surrounding widgets (a vertical rule within a row).

tool_slider(name: string; def_value: float; min_v: float; max_v: float): float

Declares a float slider in the current tool window and returns its current value – def_value until first edited; the tuned value persists in the project settings:

let radius = tool_slider("radius", 5., 0.1, 50.)
Arguments:
  • name : string

  • def_value : float

  • min_v : float

  • max_v : float

tool_slider(name: string; def_value: int; min_v: int; max_v: int): int

Declares an int slider in the current tool window and returns its current value:

let count = tool_slider("count", 10, 1, 100)
Arguments:
  • name : string

  • def_value : int

  • min_v : int

  • max_v : int

tool_window(name: string; bg_alpha: float; blk: block<():void>)

Like tool_window(name, block), additionally setting the window’s background opacity: bg_alpha in 0..1, where 1 is fully opaque. Also works for “” - the default Tools overlay:

tool_window("Scatter", 0.9) {
    ...
}
Arguments:
  • name : string

  • bg_alpha : float

  • blk : block<void>

tool_window(name: string; blk: block<():void>)

Shows the tool widgets declared inside the block in a floating tool window with this title – a normal draggable, resizable, closable window, listed in the editor’s top Window menu. Widgets declared outside any tool_window block land in the default translucent Tools overlay (”” routes there explicitly). Blocks nest. Example:

tool_window("Scatter") {
    let radius = tool_slider("radius", 5., 0.1, 50.)
    if (tool_button("scatter")) {
        scatter(get_selected_nodes(), radius)
    }
}
Arguments:
  • name : string

  • blk : block<void>

Active prefab

prefab_close()

Queues closing the prefab currently open in the editor - the prefab tab goes away on the next editor tick, like the play flow closing it before the game starts. Does nothing when no prefab is open:

prefab_close()
prefab_name(): string

The file name of the prefab currently open in the editor, “” when none is open:

tool_label("prefab", prefab_name())
prefab_open(path: string): bool

Queues opening a prefab by its project-relative path; the prefab must exist and have a .meta file. Opening happens on the next editor tick, not immediately. Returns false (and logs why) when the prefab is not found or is a read-only model prefab:

if (prefab_open("prefabs/level1.prefab")) {
    print("switching...")
}
Arguments:
  • path : string

prefab_save(): bool

Saves the prefab currently open in the editor, exactly like pressing Ctrl+S. Returns false when nothing was saved: no prefab is open, or compilation errors block the save (the editor shows a notification).

Editor and game state

focus_tab(tab: EditorTab): bool

Brings the tab to front, like clicking its header. Returns false when the tab is not open (a tab cannot be opened this way, only focused):

if (!focus_tab(EditorTab.Game)) {
    print("the game tab is not open")
}
Arguments:
game_next_frame(frames: int = 1)

Advances the paused game by frames frames (clamped to 1..1024). A stopped game is started paused first - together with game_pause this gives deterministic stepping.

Arguments:
  • frames : int

game_pause(pause: bool)

Sets or clears the manual pause flag. It is just a flag: it does not start or stop the game by itself.

Arguments:
  • pause : bool

game_play(play: bool = true): bool

Starts (true) or stops (false) the game, like the editor’s Play button. Returns false when starting is blocked by compilation errors (the reason is logged):

if (tool_button("run")) {
    game_play()
}
Arguments:
  • play : bool

game_restart(): bool

Stops the game (if running) and starts it again - picks up code changes, resets the scene. Returns false when blocked by compilation errors (the reason is logged).

game_state(): GameState

The game as the editor’s Play/Pause buttons see it: Stopped (not started), Paused (started but frozen - manually, by compilation errors or by the user menu; holds in the prefab tab too), or Playing. A stopped game with a leftover pause flag reads as Stopped. Editor tools branch on it; game code should not (and cannot ship with it - this module does not exist outside the editor):

tool_label("state", "{game_state()}")
gizmo_operation(): GizmoOperation

The current viewport gizmo mode.

is_fullscreen(): bool

True while the game view fills the window and the editor UI is hidden (like after F10).

is_tab_open(tab: EditorTab): bool

True when the tab currently exists in the editor (not necessarily focused).

Arguments:
set_fullscreen(enable: bool)

Hides the editor UI leaving the fullscreen game view (true), or brings the editor back (false).

Arguments:
  • enable : bool

set_gizmo_operation(operation: GizmoOperation)

Switches the viewport gizmo mode, like the toolbar buttons or the W/E/R/T/Y hotkeys:

set_gizmo_operation(GizmoOperation.Rotate)
Arguments:
set_shade_mode(mode: ShadeMode)

Switches the viewport shading mode, like the editor’s Options menu. Undoable; the mode persists in the project settings:

set_shade_mode(ShadeMode.Wireframe)
Arguments:
shade_mode(): ShadeMode

The current viewport shading mode.

take_screenshot()

Saves a screenshot of the game view to the screenshots folder, like pressing F11.

Work with resources and assets

find_assets_in_folder(folder: string): array<string>

Returns the project-relative paths of every recognized asset file (any type) directly inside folder (not descending into subfolders). .meta files and files with an unrecognized extension are skipped. Pass an empty string for the project root:

let levelAssets = find_assets_in_folder("levels/level1")
Arguments:
  • folder : string

find_assets_in_folder_recursive(folder: string): array<string>

Same as find_assets_in_folder, but also descends into every subfolder of folder:

let levelAssets = find_assets_in_folder_recursive("levels")
Arguments:
  • folder : string

find_resources(res_type: auto(TT)): array<TT>

Returns every alive resource of the requested type - loaded from assets and procedural alike - as an array of that exact typed id. The type<…> argument picks which resources are collected and the element type of the result; it must be a resource id type (MeshId, TextureId, …). Empty when none are alive:

for (mesh in find_resources(type<MeshId>)) {
    print("mesh at {get_asset_path(mesh)}")
}
Arguments:
  • res_type : auto(TT)

get_asset_path(id: ResourceId|MeshId|MaterialId|TextureId|SpriteId|SoundId|ModelId|AnimationId|SkeletonId|TextId|FontId|PrefabId|CollisionId|ShaderId|AnimationMaskId): string

Returns the project-relative path of the asset file backing the typed resource id (e.g. MeshId, TextureId, PrefabId, …). Returns an empty string if id is invalid or is a procedural resource with no backing asset file:

let path = get_asset_path(meshId)
Arguments:
  • id : option< ResourceId| MeshId| MaterialId| TextureId| SpriteId| SoundId| ModelId| AnimationId| SkeletonId| TextId| FontId| PrefabId| CollisionId| ShaderId| AnimationMaskId>

get_resources_in_asset(path: string): array<ResourceId>

Returns every ResourceId currently resolved from the asset at project-relative path. A single asset can produce more than one resource - e.g. a model asset also builds its meshes and materials. Returns an empty array if path does not resolve to a known asset:

let resources = get_resources_in_asset(get_asset_path(meshId))
Arguments:
  • path : string