Create an Editor Tool Extension

This tutorial will help you to create your own tool for the world editor, following some paradigms and editor APIs usage that will allow the tool to seamlessly integrate in the editor workflow.

When you have questions, make sure you check the World Editor Coding documentation first.

Step 1 - Create the extension’s Lua file

Lets call our tool just MyCoolTool, then we’ll create a new file in the /lua/ge/extensions/editor folder, as myCoolTool.lua. If you have some low level API for this tool’s operations you can create a Lua file in the /lua/ge/extensions/editor/api folder, where all the low level non-UI APIs for editor operations reside.

Step 2 - Writing the base code

The current code will be written in your new editor extension Lua file:

Base code
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
-- This Source Code Form is subject to the terms of the bCDDL, v. 1.1.
-- If a copy of the bCDDL was not distributed with this
-- file, You can obtain one at http://beamng.com/bCDDL-1.1.txt

local M = {}
local logTag = 'editor_myCoolTool' -- this is used for logging as a tag
local imgui = ui_imgui -- shortcut for imgui, please do not use just 'im'

-- here you can place your imgui calls
-- this function is called every frame when the world editor is active
local function onEditorGui()
        -- if our tool window is not shown, just return
        if not editor.isWindowVisible("myWindow") then return end
        -- setup the window, will set the default size and position
        editor.setupWindow("myWindow")
        -- begin our window, providing the visible bool ptr to be set in case of pressing X to close window
        if imgui.Begin("My Window", editor.getWindowVisibleBoolPtr("myWindow")) then
          -- your widgets here
        end
        imgui.End()
end

-- this function is a callback which we add below and called when the tools menu item is clicked
-- usually used to just show our tool window
local function onToolsMenuItem()
        -- show our window, this will set the visible bool to true
        editor.showWindow("myWindow")
end

-- this function is called when the editor is first initialized
-- it will not be called everytime after that when we toggle editor visibility
-- so just once, per game session, or when Ctrl+L pressed to reload Lua scripts
local function onEditorInitialized()
        -- this will add our tool s aa menu item in the Tools menu
        -- the first parameter is the text of the item
        -- the second parameter is a callback to be called when item is clicked
        local editorExtensionInfo = {
          experimental = false -- this extension is not experimental/wip
        }
        editor.addToolsMenuItem("My Cool Tool", onToolsMenuItem, editorExtensionInfo)
        -- register our window by name and a default size
        editor.registerWindow("myWindow", imgui.ImVec2(500, 600))
end

M.onEditorInitialized = onEditorInitialized
M.onEditorGui = onEditorGui

return M

Step 3 - Add an edit mode

If you want your tool to have a toolbar with actions, then you can use the edit modes system which lets you add an edit mode to the editor. An edit mode is a state in which the editor enters and all the actions are focused on a specific task. For example the object tool has 3 actions for move, rotate and scale which appear on the toolbar when this mode is selected. Likewise you can add your own custom edit mode. Edit modes are usually for viewport actions like scene operations (editing or creating specific objects, like road/rivers, paths, special AI tools, etc.) To add an edit mode, you will do the following:

More info: Edit Modes

Adding an edit mode
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
-- this callback will be called when your edit mode is activated,
-- be it by toolbar button click or via editor api (editor.selectEditMode)
local function testEditModeActivate()
        -- here you can prepare your tool editing mode by activating various gizmos or
        -- setting various edit state for objects/caching etc.
end

-- this will be called when the edit mode is deactivated, either by choosing another
-- edit mode in the toolbar or via code select a new edit mode
local function testEditModeDeactivate()
        -- here you can disable the edit gizmos or delete any temporary data
end

-- this will be called every frame when your edit mode is active
local function testEditModeUpdate()
        -- here you can check for example mouse ray cast scene object intersection
        -- or any other operations you may want to perform on the scene
end

local function onEditorInitialized()
        -- this basically adds your new edit mode to the editor's system
        editor.editModes.myCoolTool =
        {
                onActivate = testEditModeActivate, -- called when our edit mode is activated
                onDeactivate = testEditModeDeactivate, -- called when our edit mode is deactivated
                onUpdate = testEditModeUpdate, -- called every frame when our edit mode is active
                icon = editor.icons.some_icon_here, -- this icon will appear in the edit modes toolbar
                iconTooltip = "My Tool"
        }
        -- your tool can have many edit modes, but usually there is one per tool
        -- we'll learn below how to add a toolbar to our edit mode, in which we can place more actions
end

More info on icons: Adding New Icons to Editor

Step 4 - Add a toolbar for the edit mode

When the edit mode is active, we can add some UI, respectively toolbar buttons for our editing operations. To do that we set the onToolbar callback member of our edit mode and draw toolbar buttons there:

Adding a toolbar
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
local function myEditModeToolbar()
        if editor.uiIconImageButton(editor.icons.some_icon_here) then
                -- do something here, set some edit mode variable, do some action
        end
        -- this is for the tooltip of the toolbar button
        if imgui.IsItemHovered() then imgui.BeginTooltip() imgui.Text("My Tool 1") imgui.EndTooltip() end
        imgui.SameLine()

        if editor.uiIconImageButton(editor.icons.some_icon_here) then
                -- do something here, set some edit mode variable, do some action
        end
        -- this is for the tooltip of the toolbar button
        if imgui.IsItemHovered() then imgui.BeginTooltip() imgui.Text("My Tool 2") imgui.EndTooltip() end
        imgui.SameLine()
end

local function onEditorInitialized()
        -- this basically adds your new edit mode to the editor's system
        editor.editModes.myCoolTool =
        {
                --....................
                onToolbar = myEditModeToolbar -- set our toolbar callback, called when the edit mode's toolbar must be rendered
        }
end

Step 5 - Add edit mode undo/redo capabilities

When doing various operations in the editor, you can add undo and redo steps which is a must do, so the user experience is aided by the fact operations can be reverted. This doesnt apply to edit modes only, but also to various editing operations in general, in the following example we’ll explore both scenarios.

NOTE: Most editor API functions, especially the ones in api/object.lua have undo enabled, so for example when doing object operations they already have undo/redo. This step is mostly for your own editor operations.

Adding undo/redo
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
-- here lets define some simple variables used as test for the undo steps
local testVariable1 = 0
local testVariable2 = 0

-- the undo history action handlers
local MySampleUndoAction1 = {}
local MySampleUndoAction2 = {}

-- this will be executed when the user presses Ctrl+Z or the Undo toolbar button or the Edit->Undo menu item was clicked
-- it must set the old values which were modified
-- of course, this can get as involved as you can see it fit
-- NOTE: do not make this function local
function MySampleUndoAction1:undo(actionData)
        testVariable1 = actionData.oldValue
end

-- this will be execute when the user presses Ctrl+Y or the Redo toolbar button or the Edit->Redo menu item was clicked
-- NOTE: do not make this function local
function MySampleUndoAction1:redo(actionData)
        testVariable1 = actionData.newValue
end

-- same as in MySampleUndoAction1
-- NOTE: do not make this function local
function MySampleUndoAction2:undo(actionData)
        testVariable2 = actionData.oldValue
end

-- same as in MySampleUndoAction1
-- NOTE: do not make this function local
function MySampleUndoAction2:redo(actionData)
        testVariable2 = actionData.newValue
end

-- this will be called by our tool to just set the value of the test variable and handle undo
local function doAction1(value)
        -- we add the history action, name it, set a table with the action data, with old and new values, and the undo action class type
        local action = editor.history:addAction("MySampleAction1", {oldValue = testVariable1, newValue = value}, MySampleUndoAction1)
        -- this will bascally call the redo function of this history action, efectivelly executing the action of new value variable assignment
        editor.history:executeAction(action)
end

-- this will be called by our tool to just set the value of the test variable and handle undo
local function doAction2(value)
        -- we add the history action, name it, set a table with the action data, with old and new values, and the undo action class type
        local action = editor.history:addAction("MySampleAction2", {oldValue = testVariable2, newValue = value}, MySampleUndoAction2)
        -- this will bascally call the redo function of this history action, efectivelly executing the action of new value variable assignment
        editor.history:executeAction(action)
end

-- so in your edit mode toolbar add some action call when a button pressed
local function myEditModeToolbar()

        if editor.uiIconImageButton(editor.icons.some_icon_here) then
                -- lets just execute the action, which will also add the undo history item
                -- just set the value to something
                doAction1(123)
        end

        -- this is for the tooltip of the toolbar button
        if imgui.IsItemHovered() then imgui.BeginTooltip() imgui.Text("My Tool 1") imgui.EndTooltip() end
        imgui.SameLine()

        --.................................................
end

local function onEditorGui()
        -- if our window is not open, just return
        if not windowIsOpen[0] then return end

        --.................................................

        -- lets say we have a button inside our tool window that will call doAction2(...)
        -- remember we declared the windowIsOpen above in the previous steps
        imgui.Begin("My Tool", windowIsOpen, 0)

        -- if button pressed, it will execute the undo enabled function doAction2(...), storing a history item also
        -- if then Ctrl+Z will be pressed, the action will be undone
        if imgui.Button("Action 2") then
                -- just set the value to something
                doAction2(321)
        end

        imgui.End()

        --.................................................
end

local function onEditorInitialized()
        --.................................................
end

M.onEditorGui = onEditorGui
M.onEditorInitialized = onEditorInitialized

Step 6 - Add custom clipboard operations

When your edit mode is active the cut/copy/paste etc., are redirected to your edit mode callbacks, if present. This way you can implement custom clipboard operations for your needs. When the shortcuts Ctrl+V, Ctrl+C, etc. are issued, the edit mode callbacks are called. Same for when the menu items in Edit are clicked.

NOTE: this step is not necessary if your edit mode or tool does not require custom clipboard operations.

Adding undo/redo
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
local function onCut()
        -- cut objects/things to clipboard
        -- you can check if your tool window is focused and cut some selection
        -- for example the object tool is using this to cut objects to clipboard
        -- the asset browser is using it to cut assets to clipboard
end

local function onCopy()
        -- copy selected things to clipboard
end

local function onPaste()
        -- paste selected things from clipboard
end

local function onDuplicate()
        -- duplicate selected things
end

local function onSelectAll()
        -- select all things in the current container, be it scene or some list
end

local function onDeleteSelection()
        -- delete the things in the current selection
end

local function onDeselect()
        -- deselect the things in the current selection
end

local function onEditorInitialized()
  editor.editModes.myTool =
  {
        --...........................................
    onCut = onCut,
    onCopy = onCopy,
    onPaste = onPaste,
    onDeleteSelection = onDeleteSelection,
    onDuplicate = onDuplicate,
    onDeselect = onDeselect
        --...........................................
  }
end

--...........................................
M.onEditorInitialized = onEditorInitialized

Step 7 - Add a preferences page in the Editor Preferences

This step is valid when you want to have some settings for your tool that users can customize. To keep things tidy, we have an API to add a preferences page in the Preferences window of the editor.

More info: Custom Preferences Pages.

Adding a preferences page
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
local function myToolPreferencesUI()
        -- here you can directly call imgui widget function and manage their logic for the preferences page
        -- you can find your page in the Editor Preferences
end

local function onEditorInitialized()
--...........................................
  editor.addPreferencesPage("My Tool", myToolPreferencesUI)
end

M.onEditorGui = onEditorGui
M.onEditorInitialized = onEditorInitialized
M.onExtensionLoaded = onExtensionLoaded

Step 8 - Add custom input action map

If your edit mode has some specific shortcuts which need to override any other editor shortcuts while the edit mode is active, then you may want to set a custom input map. See Input Introduction for more info on how to create them.

Adding a preferences page
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
local function onEditorInitialized()
--...........................................
        editor.editModes.myTool =
        {
                --...........................................
                -- here we set the action map name for this edit mode
                actionMap = "MyToolActionMap"
                --...........................................
        }
end