# 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`.  ## 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). * Status messages: `ctx.status(msg)` shows a message for ~6 s; the autosave note uses `backgroundStatus`, which waits until a fresher user message has expired (it no longer replaces e.g. the glTF export result). * Inspector property labels have a fixed 118 px column; longer labels wrap onto more lines (`InspectorPanel.fitLabel`) instead of being cut ("Exclude from build"). * Readable names everywhere (`img/editor/ui_readable_names.jpg`: outliner filter "coin 3", inspector "Coin 3", status bar hover "Coin 3"/"Tile 41, 66 · Model 87", asset tab "Model 0"); inspector button rows wrap without clipping in a narrow dock (`ui_waypoint_buttons_narrow.jpg`); long property labels wrap (`ui_inspector_label_wrap.jpg`). * Narrow inspector (2026-11-07, `ui_inspector_narrow.jpg`): Position/Rotation X/Y/Z use `AxisLayout` (one row while each cell gets ≥ 66 px, else stacked one per row; also the brush rotation spinners); reference buttons (`InspectorPanel.RefButton`) show the longest name that fits ("Waypoint 5 (chain 3)" → "Waypoint 5" → "#5", other names drop a "(…)" suffix, then "…"), full name + id in the tooltip. * The naming logic lives in `editor.model.ReadableNames` (so editor.io can use it: brush compile warnings "Floor 2: … – skipped", `Validator` messages, "is still used by" errors); `ui.NodeNames` extends it. Also: waypoint added / pilot mode status, object check texts ("Coin 3: …", "Waypoint chain at Waypoint 4 (chain 2)"), missing texture page ("Texture #5 uses page 7, but the level has only 4 texture pages (0–3)"), Problems tooltip. Numbers in filter queries match whole numbers at both ends ("2" no longer finds "Floor 12"); the tile palette filter uses the same rule on model names/numbers. * Tool status and check texts use `NodeNames` too (geometry, tile paint, collision, texture paint, lights, brush-outside-grid check, "Placed Model 12 in cell …"); the outliner filter applies the whole-number rule to the old label and the id as well ("tile 3" no longer finds x = 39). ## 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, install/mod-selection/save-slot helpers for the play-test plugin (the old menu-based `play()` is gone; `level.play` is a disabled placeholder that `PlaytestEditorPlugin` replaces) | | `TaskRunner` | `runTask`: one background task on its own daemon thread; progress and completion are posted with `invokeLater` from that thread, so the completion always runs after the last progress update, also when the task or the done callback throws (replaced `SwingWorker`, whose `done()` goes through a Swing timer) | | `NodeNames` | readable names shared by outliner, inspector header, status-bar hover, reference buttons and the Tile models asset tab ("Floor 2", "Coin 3", "Waypoint 4 (chain 2)", "Tile 3, 4 · Model 12", "Model 12"); numbers follow the level order | | `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`: ```java 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)` | | `NodeNames` | `NodeNames.name(node)` (one node) or `NodeNames.of(scene)` (index for many: `nameOf`, `objectName`, `waypointName`, `chainStarts`) – use it instead of `Node.label()` in UI texts; tooltips may show `node.id()` | | `AxisLayout` | public layout for X/Y/Z cells: one row, or stacked when a cell would be narrower than its minimum (`new AxisLayout(AxisLayout.CELL_MIN)`) | | `WrapLayout` | public `FlowLayout` that grows to more rows in a narrow container (inspector button rows: `ParamsSection.compact` switches a FlowLayout bar to it) | | `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 `
`); `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 `/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 (one start camera node 2 cells behind, `PlayerStart`) 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 `/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 → `/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 `/levels/