Files
llamamod/notes/editor-ui.md
T

16 KiB
Raw Blame History

Level editor – application shell (editor.ui, phase 2)

The Swing/FlatLaf editor around the phase-1 core (editor-api.md, editor-format.md) and viewport (editor-render.md). Package com.eliasmueller.llamamod.editor.ui (+ .panels, .tools, .view). Design brief: editor-design.md.

village

Launch

  • LlamaMod header: Level Editor button next to Play (ui/MainFrame.openEditor). Same JVM, same AppPaths/Config/ModManager objects; after a play-test LlamaMod refreshes (EditorEnv.setModsChanged).
  • llamamod --editor [project.llamalevel | t1l3m002] – standalone (exits when closed). Without an argument it offers crash recovery. -Dllamamod.editor.cd=DIR uses another CD folder, -Dllamamod.editor.msaa=0 disables multisampling (software GL).
  • Programmatic: EditorApp.open(new EditorEnv(paths, cfg, modManager, false), arg) on the EDT.

Window

Area Content
Menu File (New level, Open original level, Open project, Open recent, Save, Save as, Close, Import mesh/texture, Export WAD), Edit (Undo/Redo with labels, History, Cut/Copy/Paste/Duplicate/Delete, Select all/none/similar, Hide/Show/Lock, Preferences), View (Frame, Camera ›, local space, snap, collision, dark theme, Panels ›, Layers ›), Level (Settings, Check, Build, Play), Help (Keyboard shortcuts, About)
Toolbar new/open original/open/save · undo/redo · Select Q / Move W / Rotate E / Scale R · duplicate/delete/frame · Build · Play
Options bar active tool + its options · Snap toggle, move step (1…1/16 cell), angle step · World/Local · camera view · Layers popup
Docks left: Outliner, History · right: Inspector · bottom: Assets, Problems. Tabs can be closed, moved to another dock (tab context menu), reopened in View → Panels; layout and split sizes are remembered
Viewport render.Viewport with the live scene; empty state ("Open a level or create a new one", recent projects) while no level is open
Status bar messages, hovered item, cursor position (game cells) and grid cell, counts, last build (click → Problems), progress of background tasks

Shortcuts (Help → Keyboard shortcuts lists all, incl. plugins): RMB+WASDQE fly, MMB orbit, Shift+MMB pan, wheel zoom, F frame, Home frame level, numpad 7/1/3/5 views, Q/W/E/R tools, X local space, G snap, C collision, H hide, Alt+H show all, L lock, Ctrl+D duplicate, Del delete, Ctrl+Z/Y/Shift+Z, Ctrl+X/C/V, Ctrl+A, Ctrl+B build, F5 play, F1 help. Single keys never fire while a text field has the focus; while the GL canvas has the focus a KeyEventDispatcher maps keys to actions (AWT canvases do not get Swing key bindings).

Architecture

Class Role
EditorWindow the JFrame; implements EditorContext; owns selection, viewport, SceneRenderer, docks, actions, key dispatch, document switching
EditorContext the API plugins use (document/scene/history, selection, viewport, pick helpers, execute, hidden/locked, layers, tools, status, background tasks, problems, panels)
EditorExtensions / EditorPlugin / PluginLoader registry + discovery (below)
BuiltinEditorPlugin everything built in, registered through the same API
Document Project + History + file + autosave state; dirty()
Selection ordered node set, primary = last, listeners get the source (avoid echo loops), auto-pruned
ProjectController new/open/save/save as/recent, autosave + recovery, thumbnail, import/export
PlayTest check, background build, play-test
view.SceneRenderer scene model → RenderScene, live, stable pick ids
view.Thumbnails software rasteriser for asset thumbnails
tools.SelectTool, TransformSupport, Transformers, EditOps selection/transform tools and edit operations
panels.* Outliner, Inspector, AssetBrowser, Problems, History

All edits go through ctx.execute(Command) (→ History); drags use one merge key per gesture, so a drag is one undo step (TransformSupport, inspector drag-scrub). Scene events arrive synchronously; the window collects them and flushes the renderer once per EDT turn.

Scene → render adapter (view.SceneRenderer)

  • Pick ids kind<<24 | n (PickId.TILE/OBJECT/WAYPOINT/ZONE/LIGHT, PICK_BRUSH=6, PICK_OTHER=7), assigned per node on first use, stable for the document (idOf, nodeOf).
  • Tiles: one instance per tile (mesh per TileModel, collision overlay), transform + first colour frame; a CHANGED tile only updates its instance (RenderScene.changed(inst) → one chunk rebuild); a changed model rebuilds its mesh and its tiles. Objects: model via StratModels from the STPC raw chunk (same rules as LevelSceneAdapter), markers for logic objects. Markers/overlays (waypoints, zones + teleport targets, lights, regions, brush boxes) are rebuilt as a whole (RenderScene.overlaysChanged(), cheap). Texture/STPC changes or RELOADED rebuild everything (~0.3 s for the village).
  • Brushes: BrushCompiler.compile (the build's own code) for a live preview; generated tiles get the pick id of the smallest enabled brush whose bounds contain them, plus an outline box and handle marker.
  • Waypoints default to "selection only" (chains of selected objects/waypoints), View → Layers can show all.
  • Hidden nodes: instance/marker visible=false (renderer change: invisible tiles leave the static batches); locked nodes are drawn but pickNode ignores them.

Extension API (phase 3)

Rule for parallel agents: never edit editor.ui itself. Create a package of your own, e.g. com.eliasmueller.llamamod.editor.tools.waypoints, with a class whose simple name ends in EditorPlugin:

public final class WaypointEditorPlugin implements EditorPlugin {
    @Override public void install(EditorExtensions x) {
        x.tool(new WaypointTool());                                         // toolbar + shortcut
        x.panel(new PanelSpec("waypoints", "Waypoints", "waypoint", Dock.RIGHT, 30, false, WaypointPanel::new));
        x.action(ActionSpec.of("wp.newChain", "Tools", 10, 10, "New waypoint chain", "waypoint", "ctrl shift W",
                ctx -> ...).enabledWhen(ctx -> ctx.document() != null));
        x.inspector(new ChainSection());                                    // extra inspector content
        x.layer(new LayerSpec("wpLabels", "Waypoint labels", "waypoint", false, 55));
        x.renderContributor(new ChainLabels());                             // overlay geometry in that layer
        x.check((scene, out) -> ...);                                       // build/check problems
        x.transformer(new MyNodeTransformer());                             // gizmo support for a node kind
        x.template(new MyLevelTemplate());                                  // "New level" start content
        x.assetTab(new AssetTab("prefabs", "Prefabs", "assets", 50, PrefabTab::new));
        x.onOpen(ctx -> ctx.addSceneListener(e -> ...));                    // runs once the window exists
        AssetDrop.register("prefab", (ctx, data, ground) -> ...);          // drag & drop into the viewport
    }
}

Discovery: PluginLoader scans the classpath (jar or class folders) for *EditorPlugin.class below com/eliasmueller/llamamod/editor/ (no shared registration file); -Dllamamod.editor.plugins=a.B,c.D adds others. A plugin that throws is skipped with a message. Built-ins install first; registering the same id again replaces (panels, tools, actions, asset tabs, layers).

API Notes
Tool id/name/icon/tooltip/shortcut/group/order; activate/deactivate; mousePressed(ToolEvent) → true = owns the drag (else default click-select); mouseDragged/Released/Moved, keyPressed; gizmoMode() (null = no gizmo) + gizmo(ctx, GizmoEvent) + placeGizmo(ctx); drawOverlay(depthTested, onTop) (export space, call ctx.repaintTool()); optionsBar(ctx); inspectorSection(ctx, sel); cursor(). Right/middle mouse and Alt always stay with the camera
ToolEvent x/y/shift/ctrl/alt/clickCount, lazy pick() (respects layers/hidden/locked), surface() (geometry point), onPlane(y), ground(), toGame(p) / toExport(x, y, z)
ActionSpec menu path "Tools" or "View/Camera", group/order, label, icon, KeyStroke string, run, enabledWhen, checked (check box item), tip; x.toolbarAction(id, section)
PanelSpec id, title, icon, default Dock, order, visible by default, factory ctx → JComponent (created once; follow addDocumentListener/selection().addListener/addSceneListener)
InspectorSection appliesTo(sel), create(ctx, sel) (rebuilt on selection change), replacesProperties()
RenderContributor layer(), affectedBy(SceneEvent), followsSelection(), build(ctx, builder) → builder.overlay() lines/triangles, builder.marker(node, x, y, z, rgba) (pickable → selects node)
NodeTransformer position (game cells), yaw (deg or NaN), scale (or null), set(node, pos, yaw, scale) → prop map. set must compute from the node it gets (the shell passes a drag-start snapshot)
LevelTemplate fill(Scene) adds nodes directly (no undo) before the editor shows the new level
LevelCheck adds Problem.error/warning/info(msg, node); errors block Build/Play
Icons vector icons by name; Icons.register(name, painter) for your own (16×16 Java2D); Icons.swatch(colour, w, h)
Widgets, util.SwingListeners Widgets.button(text, icon, tip, run), Widgets.listCell(c, list, selected), Widgets.wrappingLabel(html) (help text that wraps at the dock width – use it instead of <body style='width:…'>); onTextChange, onMenuOpen
ErrorDialog ctx.error(title, t) uses it: plain sentence (ErrorDialog.plain(t), no exception class names) + collapsible Details (stack trace)
Game state ctx.setGameRunning(what, stop) (default no-op): while a play-test game runs the status bar shows "Game running: what" and the toolbar Play button becomes Stop game (runs stop); null resets. PlaytestRunner sets it on launch and clears it when its watcher sees the game exit
AssetDrop register(kind, handler); dragSource(list, value → payload(kind, data)) makes a list draggable into the viewport
EditorPrefs ctx.prefs() JSON settings in <data>/editor/settings.json; prefix your keys
NodeCopy duplicate/snapshot nodes (fresh ids)
EditOps (tools) placeTile, placeObject, copies, duplicate, delete, … reusable from plugins

Coordinate reminder: render/viewport points are export space (z = −game z); model positions are game space (ToolEvent.toGame / toExport). Drop/placement points: ctx.groundAt(x, y), ctx.viewCentre(). Commands: editor-api.md (Edits, SetProp, Batch with merge key).

Render additions used by tools (backwards compatible, editor.render)

Viewport.setHudOverlay(pixels) (second pixel overlay independent of the tool, e.g. the Shift+F5 crosshair), Viewport.setInput(ViewportInput) (tool hook: left-button press/drag/release not on the gizmo, moves, keys), setToolOverlay(depthTested, onTop), setScreenOverlay(pixels) (marquee), unproject(x, y, depth) public, isFlying(), canvas(); RenderScene.overlaysChanged(); LevelSceneAdapter.mesh/page/collision public; invisible tile instances are no longer baked into chunks; History.undoLabels()/redoLabels().

Project lifecycle

  • New level: dialog (name, author, base level by name with its loading picture, TestLevel chapter = next free one, template Empty / Flat floor / Island). Templates add brushes, a Llama (camera node 8 cells behind, as levelbuild) and a LevelDoor on a material-16 floor.
  • Open original level: list "Village – Chapter 1" … "Lab – Chapter 5" (+ unused/front-end WADs) with the CD loading pictures (OriginalLevels, Cor).
  • Save (.llamalevel, default folder <data>/levels = listed as mods in LlamaMod), Save as, Open recent (10), dirty marker "•" in the title, confirm on close/new/open. Thumbnail: viewport screenshot 640×360 PNG on every save (also the TestLevel loading picture).
  • Autosave every 90 s (Preferences) while dirty → <data>/editor/autosave/*.llamalevel + .json (title, original file, time); deleted on save/close/discard. After a crash the next start offers Recover / Discard / Later (verified: kill during editing → recovered with all edits).

Build & Play-test

  • Check: Validator + checks (no player start = error, no exit in a new level = warning, no geometry, brushes outside the grid). Messages with node ids are double-clickable in Problems.
  • Build (Ctrl+B): validates, snapshots the project (ProjectIO.write), then reads + builds the WAD in the background (UI stays usable) → Problems: "Build OK: tiles (from brushes), objects, waypoints, KB, time" + warnings.
  • Play (F5): build → write the current state to <data>/levels/<stem>.llamalevel (if that is the document's own file it counts as saved; otherwise the user's file stays untouched) → enable the level mod next to the active mods (turns off other mods writing the same WAD, incl. Test World on chapter 1), TestLevel chapter option = project chapter → ModManager.apply → save slot (Preferences, default 3; backup first) unlocked up to the level if needed and map cursor on it → start the game.
  • Boot to level (phase 3, editor.tools.playtest, notes/editor-playtest.md): Play now also applies the exe mod boot-to-level, so the game loads the level directly (no menus) and Escape quits; Shift+F5 = play from the camera position (holding Shift or hovering the action shows a green/red crosshair at that point). While the game runs: "Game running" badge + Stop game button (see Game state). The old menu-based run is still available (Level → Play boots straight into the level off). -Dllamamod.editor.noLaunch=true skips the launch (tests start the game through apptest).

Tests

  • EditorShellTest (CD needed, -Dllamamod.cd): drag = one undo step + world-grid snapping, rotate about a pivot, duplicate with waypoint-chain copy + delete/undo, brush move/scale, renderer live updates (tile move, marker move, delete/undo, waypoints of the selection), templates build brush tiles + thumbnails render, plugin discovery, number-field arithmetic.
  • EditorTour (test classes, not a unit test): scripted run on an X display, writes the screenshots below. java -cp all.jar:build/classes/java/test -Dllamamod.data=DIR -Dllamamod.editor.msaa=0 com.eliasmueller.llamamod.editor.ui.EditorTour OUTDIR village,select,move,… (steps: empty, recover, village, select, move, rotate, marquee, panels, layers, build, drag, newlevel, place, save, play, crash, dark, shortcuts, dialogs).
  • End-to-end (2026-11-06, Xvfb :8 editor, :7 game): new level "Editor Island" (Island template, chapter 7) → 2 coins + checkpoint placed, checkpoint duplicated → saved → Build OK (1830 brush tiles, 6 objects, 5.6 MB) → Play (noLaunch: mod applied, slot 3 unlocked, cursor on TestLevel 7) → apptest start/tolevel --world 9: slot shows "TestLevel Kapitel 7", the island loads with trees, coins and checkpoints, the llama walks (img/editor/ui_e2e_game.jpg). Drag & drop from the asset browser into the viewport placed an object (Robot drag). Crash recovery verified.

Screenshots (img/editor/ui_*.jpg): empty state, village, select+move gizmo, moved, rotate, marquee (+ result), assets (tile models, objects, textures), level settings, layers (collision), build, drag & drop, new island, placed objects, play-test dialog, dark theme, shortcuts, new-level and open-original dialogs, LlamaMod header button, game.

Known limits / next steps

  • Rotation only about the vertical axis (rx/rz via the inspector); scale only for brushes.
  • Brush preview recompiles all brushes on every brush change (fine for dozens; a large level may need per-brush compilation). Generated brush tiles are picked by bounds heuristics.
  • Tile models without tiles have no "place" preview; dropping places in the cell under the cursor with grey vertex colours (no lighting bake for original tile models).
  • Clipboard is in-memory (same window), paste offsets by one step per paste.
  • The LlamaMod mod list shows a newly installed play-test level only after restarting LlamaMod (AppState.registry is created once).
  • Not tested on Windows hardware (the viewport was tested under Wine in phase 1).