Editor Overview
Overview
The editor is enabled by -DTNX_ENABLE_EDITOR=ON. It runs on the Encoder (render) thread and operates directly on live ECS slab data — not a copy. Scope is intentionally limited to scene editing, entity inspection, and PIE testing.
Planned changes. Editing moves onto the Edit Timeline: Brain becomes the only slab writer, edits commit as frames, and undo/redo becomes transaction-based with the history ring as its fast path. This page describes the current implementation.
Panels (7): World Outliner, Details, Content Browser, Engine Stats, Log, Component Generator, Debugger, all in GG-UI.
Editor windows (3): Construct Editor, Entity Editor, Prefab Editor — authoring windows for Constructs, entity types, and prefabs. The Construct Editor uses TrinyxParser to turn annotated Construct C++ methods into node graphs.
Windowing: panels dock in the main window or float in their own OS windows (docking).
Core Components
Owns the editor's state and its panels. Its frame runs on Encoder before each render.
- Panel registration via
AddPanel<T>(...) - Scene load/save (
LoadScene,SaveToFile) - Entity spawn/delete via the engine spawn handshake
- Prefab instantiation (
SpawnPrefab) - Undo/redo stack (max 50 commands, merge-capable)
- PIE lifecycle (
StartPIE,StopPIE) - Scene snapshot/restore for play/stop cycles
- The scene view (
SceneView): picking, camera controls, the gizmo, prefab drops - File drag-drop handling
Writing a panel
A panel is GG-UI content in a dock panel. Three rules keep it short and current:
- Watch once, at the top.
ui.Watch(Changed)covers everything built inside: tabs, split panes, popups. A panel that shows editor state also watcheseditor.Changes(). - Change the editor with
Do. Handlers run in the UI update, where the editor and the engine aren't theirs to change.editor.Do([](EditorContext& Editor) { editor.SaveScene(); })runs on Encoder before the next frame and then movesChanges(), so nothing is bumped by hand. - Find what a handler changes by a stable ID, not an index: tabs close and reorder between the build and the click. The type editors keep their documents as
unique_ptrs with anIDand change them throughEdit(id, fn), which does nothing if the document has closed.
void Build(UI& ui, EditorContext& editor) { ui.Watch(editor.Changes()); ui.Button("Editor.Scene.Save", [&editor] { editor.Do([](EditorContext& e) { e.SaveScene(); }); }); }
Transient per-frame state shared across all panels. Lives on the render thread — no synchronization required.
| Field | Description |
|---|---|
SelectionType | None / Archetype / Entity |
| Selected entity triple-index | ClassID, Archetype*, Chunk*, LocalIndex, CacheIndex |
SelectedEntities | Every selected entity; the last is the one the fields above name |
GizmoOp | Translate / Rotate / Scale |
bGizmoWorldMode | World vs local space |
GizmoSnapTranslate/Rotate/Scale | Snap increments (default 0.5 / 15° / 0.25) |
bPivotCentre | Gizmo in the middle of the selection, or on the last one's origin |
bShowGrid | The ground grid |
CurrentScenePath / CurrentSceneName | Loaded .tnxscene |
bSceneDirty | Unsaved-changes flag (shown as * in menu bar) |
SceneDefaultState / SceneDefaultMode | FlowState and GameMode names for PIE |
| Engine references | Registry, EngineConfig, LogicThread, TrinyxEngine, MeshManager, AssetDatabase |
ReplicationSystem* | Non-null only during networked PIE |
Panels
Editor* in src/Editor; the type editors are EditorPanels in src/Editor/Private/Panels.
Outliner (
What is in the edit world, as a tree: By Owner (each Construct with the entities its views made, then the entities nothing owns, by archetype) or By Type (every entity, by archetype). A name filter. Its selection is the editor's, both ways: picking rows selects in the viewport (Ctrl and Shift for several), and selecting in the viewport selects and reveals the rows. Double click frames; right click has Frame, Drop to Floor, Duplicate and Delete. A prefab dropped on it is placed. The outline is rebuilt when the world's shape changes, and once a second.
Not yet: parenting and attachments (the engine has none yet); prefab instances; renaming entities (they have no names).
Details (
What is selected. Entities: a property grid over their components (EntityFields), several selected edited together as one undo step, with a field filter. Asset references (mesh, material, skeleton, animation) are picked by name from the loaded assets of their type. An archetype: its components and their tiers. A Construct: its type and ID. While the simulation runs a warning says values change under you.
Not yet: Construct fields; picking an asset that isn't loaded; materials (no material manager, so there is nothing to list); dropping an asset on a field.
Content Browser (
The project's content and src folders as a tree. Content: tiles (sized by a slider) or a list, a search, a type filter, breadcrumbs, Import. Double click opens a level or a prefab; right click renames, deletes, or creates a folder, level, prefab or data asset. Source: the headers in a folder and whether each is a Construct, Entity or Component; double click opens the Construct or Entity editor; right click creates one. An asset is dragged as its AssetID: a prefab onto the viewport (shown where it would land) or the outliner. Folders are read when the panel first shows, after it changes them, and on Refresh.
Not yet: thumbnails; moving assets between folders; tiles for very large folders are all built (no virtualisation).
Engine Stats (
The Brain and the Encoder against their budgets (the fixed step, and the display's refresh); the simulation's health, bound to EngineStats (Sentinel rate, how far behind real time, rollbacks, frame age, GPU frame time from timestamps, UI primitives); the edit world's entities; the GPU (device, driver, Vulkan version, video memory used and budget, from VK_EXT_memory_budget where the driver has it); the engine configuration.
Log (
The logger's recent lines, filtered by level, channel (Engine, Game, UI) and text. Clear hides what is there so far. A command line runs TnxConsole commands. Lines are numbered so a filter change never reuses one.
Node graphs (
The graphs in the Construct and Entity editors: a tick slot's or a TNXFUNC method's, and an entity type's spawn initializer. A NodeGraphCanvas holds a graph's nodes and wires; Build shows it with ui.Graph and makes each gesture one step of the graph's own undo, which Undo and Redo use while focus is in the graph. A right click opens a searchable menu of node kinds (events, flow, properties, maths, logic, vectors, entity, input, variables, values), placed where it was opened. Wires join only pins of matching kinds (exec to exec, a value to the same type, Int into Float, anything to Any); an in pin takes one wire. Copy and paste work across graphs.
Component Generator (
A component's name, storage tier, system group and fields, with the header it makes shown as it is edited. Export writes the header; Register as Script registers it at once as a script component (stored per entity, no SIMD, no rollback) until the header is compiled in. Emits the tier's macro (TNX_TEMPORAL_FIELDS / TNX_VOLATILE_FIELDS / TNX_REGISTER_FIELDS).
Construct, Entity and Prefab editors
A tab per open type or file. Opening one from the Content Browser brings its editor to the front (EditorPanel::).
- Construct Editor: views and owned members; a graph per tick slot and per
TNXFUNCmethod, parsed from the source byTrinyxParser; node counts. - Entity Editor: the type's components, picked from the registered ones; its spawn graph.
- Prefab Editor: an entity prefab's values in the same property grid as Details (
PrefabFields, over the prefab's JSON; a field it doesn't name is zero, its default), a Construct prefab's named defaults, Save and Revert, and a live preview, framed to fit, to orbit and zoom. Each open prefab has its own undo: while focus is in the panel, Undo and Redo are the prefab's (a command nearest the focus wins), otherwise the scene's. One edit in the grid is one step; typing on in one field is one step; Revert is a step too. - Prefabs are centred. Save Prefab leaves out the position (
PosX/Y/Z,VisPosX/Y/Z): a prefab is placed where it is spawned. Rotation and scale are kept. The preview centres older prefabs that still have one.
Not yet: generating a Construct's or an entity type's header from its graphs. Their parts (values, graphs, preview) are splits inside one panel; they are to become dock panels that follow the tab in front, as the widget designer's are, so every tool's parts can be arranged and saved in layouts.
Widget Designer (
Widget assets (.tnxwidget) edited as their files and shown as they will look (Widgets). Opening one from the Content Browser brings up the UI workspace.
- Widget Designer: a tab per open asset. A HUD is shown on a screen, chosen from presets of common devices or set as a size in pixels and a DPI, and turned round; any other asset in an area of a size you set. The canvas is a document of its own (
ui.Document) built from the host's preview library, which holds the asset as it stands in the designer; the running game builds from the saved files only. - Widget Problems: what loading the asset as it stands reports, and what building it on the canvas reports.
- Saving writes the outline layout (
GGWriteWidgetFile), keeping the keys' order and the file's comments, with the editor's safe writes. Each asset has its own undo while focus is in the designer. Unsaved assets are in the exit question, Save All, and autosave, as prefabs are (EditorDocuments). - Widget Hierarchy: the asset's elements as a tree, with the asset each use uses. Drag rows to reorder and reparent them. A row's menu, or the keys while focus is in the designer: Rename (F2, typed in place, and what names it in animations and triggers follows), Duplicate (Ctrl+D), Copy, Cut, Paste (Ctrl+C, X, V: copies get names of their own, and the text goes to the clipboard too), Delete, Wrap With, Replace With, and Turn Comment into Note.
- Widget Palette: the element types and the widget assets (the engine's, then the project's), searchable. Drag one onto a hierarchy row (before, in, or after it) or onto the canvas (into what is under the pointer, or after it), or click it to add it where the selection is.
- Zoom and pan: the wheel zooms the canvas about the pointer, a middle or right drag pans it; Fit (or F) shows it whole again, and 1:1 shows the asset's units at the editor's. Text is shaped for the size it is drawn at, so it stays sharp zoomed in.
- Selecting on the canvas: what the pointer is over is outlined, and a press selects it, with the hierarchy. The canvas's document tags each node with the element that built it (
UIContext::SetTagsElements). - The edits are
GGWidgetFileoperations on the file's JSON (UIWidgetFile.h), each one undo step. - What it is shown on is kept in
editor/previews/<asset ID>.jsonin the project, never in the asset. TNX_EDITOR_OPEN=<file>;<file#Element>opens widget assets at start (selecting an element),TNX_DESIGNER_SELFTEST=<a copy of an asset>works the designer's edits, undo, and saving through it and logs each check, andTNX_SCREENSHOT=<file.bmp>writes what the window shows afterTNX_SCREENSHOT_FRAMEframes (300 by default): for checking it without driving the desktop.
Not yet: Details, Inputs and Contexts (8c), Styles and Cost (8d), Animation (8e), canvas tools (8f).
Debugger (
Network: active channels, dirty entities, state correction and entity delta bytes per dispatch and their ratio, with 120-sample graphs (drawn on a canvas). The last session's numbers stay after it stops. Profiler: the fixed step and the logic frame, with their history against the budget. Render Debug (builds with TNX_DEBUG_RENDERING): the debug draw mode.
Dialogs, Palette and Status Bar
- Files. Open, Save As, Save Prefab, Import and Reimport use the platform's file dialog (
EditorContext::AskForFile); the answer comes back to the Encoder. Scenes save as.tnxscene, an entity saved as a prefab as.prefab, in the scene's form for one entity (EntityBuilder::: values by component, asset references by identity).SerializeEntity - Unsaved changes. See SavingSaving
.
- Missing assets. Registered assets missing from disk are listed at startup, each with Reimport (meshes and skeletons) and Forget.
- Command palette. Ctrl+K, or View > Command Palette. Lists every editor action (
EditorActions::All) by its title in the interface language; Enter runs the first match. - Status bar. The scene's file, and the Brain, Encoder and GPU times.
Saving
The scene and the prefabs are written only when saved. Content Browser actions (new, rename, delete, import) and the editor's own files (layout, key bindings, settings) are written when they happen.
- What is unsaved. The scene is unsaved while the undo history stands anywhere but where it stood at the last save or load (
UndoHistory::), so undoing back to it clears thePosition *. An edit with no undo step (the scene's play settings, a scene merged in) keeps it unsaved until saved. A prefab is unsaved while its JSON differs from what was last read or written. - Before leaving. Opening another scene, closing the window, Alt+F4, the title bar's X and File > Exit list what is unsaved (
EditorContext::): each document ticked, then Save Selected, Don't Save or Cancel. Opening a scene lists only the scene; leaving the editor lists the open prefabs too. A save that fails, or a Save As that is cancelled, keeps the editor where it was. Closing a prefab tab with changes asks about that prefab. Closing goes through the engine's exit guard (AfterUnsaved TrinyxEngine::GuardExit,AskExit): while it is set, a request to close is only noted, and the editor decides. - Save, Save All. Ctrl+S saves the scene, or the open prefab while focus is in the Prefab Editor; a scene never saved asks where. Ctrl+Alt+S saves the scene and every changed prefab.
- Safe writes. Every save goes to a file beside the target and then replaces it (
WriteFileSafely), so a crash or a full disk never leaves a half-written scene, prefab, layout or settings file. - Autosave and recovery. Every
autosaveMinutes(editor settings, 5 by default, 0 for never), unsaved documents are copied toSaved/Autosave/, never over their own files. A clean exit removes the copies. If the editor finds them at start, it offers to restore them: the scene and prefabs come back under their own names, unsaved.
Asset Database
UUID↔path authority. Populated at startup; used by the runtime AssetRegistry.
Sidecar files (.tnxid): Binary, 8 bytes UUID + 8 bytes ContentHash + 4 bytes SchemaVersion + 1 byte Flags. Created once per asset at import.
SidecarFlags: Dirty (content hash mismatch), SchemaOutdated (importer version changed).
Reconciliation: Runs at Initialize() and on-demand. Scans content directory, creates .tnxid for new assets, marks Dirty on hash mismatch, detects moved assets (UUID stable, path updated).
Persistence: .tnxdb file in content root.
Scene File Format (.tnxscene)
JSON:
{ "name": "TestArena", "defaultState": "GameplayState", "defaultMode": "ArenaMode", "entities": [ ... ] }
Per-entity fields serialized as named key-value pairs. Prefab files are single-entity .tnxscene files.
Load: JSON → EntityBuilder:: via spawn handshake → update EditorState metadata.
Save: SaveToFile() serializes all registry entities.
Play-In-Editor (PIE)
Local play:
SnapshotScene()captures all archetype field data- Simulation unpauses; input routes to the active world
StopPIE()→RestoreSnapshot()byte-restores all field data
Networked PIE (1–4 clients):
- Each client gets its own
WorldViewport,FlowManager, and engine config instance - Server may be headless (no viewport) or rendered
ReplicationSystemis activated on the server world- PIE uses real GNS loopback — bugs found in PIE are real bugs
Play views: Solo plays in the scene view. The Authority and each Owner get a dock panel holding a ui.Viewport whose image follows the panel's size. A click in a view gives its world the input.
Keys while playing: the game has the keyboard and pointer, except the chords of actions kept from the game (UIHost::): Stop (Escape) and Release Pointer (Shift+F1, back to the editor and back to the game again). Both can be rebound in Keybinds.ini.
Scene View
SceneView is the edit viewport in GG-UI: the scene's render target as a ui.Viewport, sized by layout, with a ui.Canvas over it for the gizmo, selection boxes, the box being dragged, and an axis triad. Its handlers only record the pointer; Update, on Encoder at the top of the editor's frame, acts on it.
| Input | Does |
|---|---|
| Left click | Select; Shift adds, Ctrl toggles, empty space clears |
| Left drag on nothing | Box select; Shift adds, Ctrl takes away |
| Left drag on a gizmo handle | Move, rotate or scale; Ctrl snaps while held (or stops snapping); Alt drags a copy |
| Right drag | Fly: mouse look, WASD/QE (rebindable, Editor.Camera.*), the wheel sets the speed |
| Alt + left drag | Orbit the selection (or a point ahead) |
| Middle drag | Pan |
| Alt + right drag, wheel | Dolly |
| Right click | Context menu (a right drag opens none) |
- Gizmo: drawn on the canvas, always on top. Move has axis arrows, plane squares and a centre for the screen plane; rotate has a ring per axis (the half facing the camera takes the pointer) and a screen ring; scale has axis boxes and a centre box for all three. It keeps a constant size on screen. World or local space (scale is always local); the pivot is the selection's middle or the last one's origin. A drag works on every selected entity and is one undo step; values are written from where the drag started, so nothing drifts.
- Grid:
viewport_grid.frag.slang, a full-screen pass after the scene that cuts each pixel's view ray with y = 0 and writes that depth, so the scene hides it and it hides what is below. Lines every unit and every ten, the X axis red and Z blue, fading where cells get small and toward the horizon. - Prefab drops: a prefab dragged over the view is spawned and follows the pointer, resting on what is under it (a physics ray), else the ground plane; it snaps to the grid when snapping is on. Let go, it is placed as one undo step and selected; taken away or Esc, it is destroyed. The drag is a GG-UI drag of the asset's
AssetID; the view hears it through its drop target's OnOver. - Keys:
Editor.Viewport.*actions run from a key only while the view has the pointer or the keyboard (a press in it focuses it); from a menu or a button they always run.
Not yet: view modes (unlit, wireframe) and show flags beyond the grid; a selection outline in the scene (selected and hovered entities get a box); cycling through what is under a repeated click; vertex and surface snapping; orthographic views, camera bookmarks, field of view.
Planned: Construct spawners. Scenes hold ECS data only, so a Construct is placed through an entity: a spawner, placed like a prefab, whose component names the Construct type or Construct prefab and the values it starts with. In the editor it shows a preview of the views the Construct would create; when play starts or the level loads, it creates the Construct at its transform. It builds on ESpawnPoint.
Undo / Redo
Stack-based, max 50 commands. Adjacent compatible commands merge. To be replaced by transactions on the Edit Timeline.
| Command | Merges? |
|---|---|
EntityTransformCommand | Yes — same entity (JSON field snapshot) |
ComponentFieldChangeCommand | Yes — same field (old/new bytes) |
Ctrl+Z / Ctrl+Y.
Keyboard Shortcuts
The defaults. Every action can be given other chords, or a second one, in Keybinds.ini.
| Key | Action |
|---|---|
| Q / W / E / R, Space | Select / Move / Rotate / Scale, next tool |
| Ctrl+Z / Ctrl+Y | Undo / Redo |
| Ctrl+O / Ctrl+S / Ctrl+Shift+S / Ctrl+Alt+S | Open / Save / Save As / Save All |
| Delete / Ctrl+D | Delete / duplicate the selection |
| F / End | Frame the selection / drop it to the floor |
| X | World or local space |
| Shift+Space | Maximize the panel under the pointer |
| Ctrl+K | Command palette |
| F5, comma / period | Play, step back / forward |
| Escape | Stop playing |
| Shift+F1 | While playing, give the pointer and keys to the editor, or back to the game |
GPU Picking
Enabled with TNX_ENABLE_EDITOR=ON. The edit viewport has an R32_UINT pick target, the entity cache index per pixel, drawn only in frames that ask (EditorRenderer::). A request is a rectangle: a click asks for one pixel, a box select for the box, and the pointer resting over the view for the pixel under it (the hover box). The rectangle is copied to the frame slot's readback buffer and read when that slot comes round again; the answer is each entity in it once (TakeScenePick).