# Level editor – texture tools (`editor.tools.textures`) Plugin `TexturesEditorPlugin` (phase 3). User guide: `editor-guide.md#textures`. ![texture manager](img/editor/tex_manager.jpg) ## What it adds | Item | Where | | |---|---|---| | Texture manager | panel `textures` (bottom dock, Ctrl+Shift+T; **Window** button = own 1300×760 frame) | pages, atlas, table, details, animation editor | | Import texture… | File menu (replaces the built-in `file.importTexture`), panel toolbar | `ImportDialog` | | Replace texture pixels… | Edit menu, panel toolbar | image → the selected texture's rectangle | | Paint texture | tool `texpaint`, key **M** (own toolbar group) | `PaintTool` | | Texture eyedropper | Edit menu, key **I**; Shift+click in the paint tool | | | Checks | Build/Check | > 127 pages = error, entry on a missing page = error | `TexState` holds the window's *current texture* (shared by panel, tool, eyedropper; selecting a `TextureEntry` anywhere, e.g. in the outliner, makes it current). ### Texture manager (`TextureManagerPanel`) * **Pages**: thumbnail, index, blend class of the page format (`Blend.ofPage`). Context menu: export page PNG, replace page from PNG (scaled to 256×256, reduced with the page's class). * **Atlas**: the page (nearest-neighbour, fit to the view until the wheel zooms) with all rectangles: white = used, red = unused, cyan = animated, orange = current with 8 handles. Drag inside the current rectangle = move, handles = resize (one undo step per drag: `TextureLibrary.setRect` with a merge key). Click selects the smallest rectangle under the cursor; double-click activates the paint tool; tooltips show size/usage. * **Table**: #, preview, name (editable), page, rectangle, blend, faces (tile-model faces + brushes), models, animation ("frame" = used as a frame). Filters: all / on this page / used / unused / animated / imported; search by name or number. * **Texture tab**: name, X/Y/W/H spinners, blend class (sets the entry flags), raw flags, usage line, *Duplicate* (new entry with the same rectangle, e.g. for frames), *Select users* (tiles whose models use it). * **Animation tab** (`AnimEditor`): create/remove, type flip-book / scroll up / scroll down, frame delay (≈ fps at 30 game frames/s), back and forth, frame list (+ − ◀ ▶), scroll band / speed / top row, live preview (`AnimSpec.frameAt/offsetAt`). Usage counts (`TextureLibrary.usage`): faces of TRAK tile models (`faces[i+4]`), brush string parameters `"#n"`, animation targets and frames. Object (STPC) models are not counted – their textures are in the same table, so "unused" means *not used by tiles/brushes/animations*. ### Import (`ImportDialog`, `ColorReduction`, `TextureLibrary.importItems`) ![import dialog](img/editor/tex_import.jpg) * Size = longest side (16…256 or original), aspect kept; rectangles need not be powers of two. * **Blend classes** (`Blend`), derived from the flag pairs of all original level WADs (entry flags ↔ page flags & 0x7f, counts over 33 WADs): | Class | entry flags | page flags | originals | |---|---|---|---| | Opaque | 0x0104 | 0x81 | 16 551 entries | | Cut-out | 0x0406 | 0x84 | 1 774 | | Semi-transparent (mode 1) | 0x1406 | 0x87 | 528 | | Additive (mode 2) | 0x2406 | 0x85 | 649 | | Darken (mode 3) | 0x3406 | 0x82 | 574 | Others seen: 0x1104/class 6 (186), 0x2104/class 3 (38), 0x4406/class 5 (469), object-model entries 0x8cc6/0xbbc6/0x9cc6/0xb8c6 (bit 15). Entry bits 12–14 = blend mode, 0x400 = has transparent pixels. * **Colour reduction** to ABGR1555: alpha < cut → 0x0000; with "keep half-transparent" alpha < 192 → STP 0 (black becomes 0x0421, never 0); optional colour key (magenta / top-left pixel) and 4×4 ordered dithering; opaque class ignores alpha. Preview = the game's 15-bit result. * **Packing**: with "pack into free space" the importer searches pages of the same format class for a free block (`FreeMap`, summed-area table over a 256×256 occupancy mask built from all rectangles plus scroll bands), 8-pixel grid, largest images first; otherwise / if full it appends pages (≤ 127). Optional 1-pixel border (edge pixels repeated) against filtering seams. One undoable `Batch`: `SetProp pixels` of changed existing pages (the project stores only the changed pixel runs), `AddNode` pages, `AddNode` entries (always appended – faces reference the index). * Village test: two 32×32 imports land in free space of existing pages, a 200×100 semi-transparent one gets a new 0x87 page; the WAD builds and re-imports (`TextureLibraryTest`). ### Paint tool (`PaintTool`) ![paint](img/editor/tex_paint.jpg) * Hover shows the face under the cursor (white outline) and model/face/texture/flags in the status bar (plus "model shared by n tiles"). Face picking: `ctx.pickNode` → tile, then a Möller–Trumbore ray test against that tile model's triangles (tile transform, export space). * Click = assign the current texture; scope **Face / Same texture in model / Whole model**. Shift+click or the eyedropper button / **I** = pick. The clicked face stays selected (orange). * **Rotate** cycles the face's corners (v0,v1,v2 → v1,v2,v0; winding and plane unchanged), **Mirror** toggles 0x20 (mirror U), **Other half** cycles 0x800/0x10 (`Geometry.faceUvs`: no 0x800 = corners (a,v1)(b,v1)(a,v0); 0x800 = (a,v1)(b,v1)(b,v0); 0x810 = (b,v0)(a,v0)(a,v1)). * **Unique**: if the tile's model is shared, the edit first adds a copy of the model and points the tile at it (one undo step). * Brushes: the clicked side's normal (from three depth samples) picks the parameter `top` (n.y > 0.7) / `bottom` / `side`, else the first texture parameter (`tex`, `post`, `trunk` …); the value becomes `"#n"`. Rotate increments `texRot` (0–3) of the last painted brush. ## Texture animations (decoded, `AnimSpec`) Loader FUN_00406e40 (16-byte runtime records at 0x581138, count 0x581134), update FUN_0040c6e0, called once per rendered frame from FUN_0054b590 while the game is not paused. The runtime texture record (20 bytes at 0x581154 + tex·0x14) holds the page and the UVs as floats (+0xc / +0x10 = v0 / v1, divided by the page height at 0x58114c + page·0x20 + 8). | Type | File payload | Behaviour | |---|---|---| | 8 flip-book | delay, n, pingPong, n × u16 texture | every delay+1 frames the texture's runtime record is replaced by the next frame's (loop or back and forth). 214 in the levels (torches, water, flags), e.g. `03 08 00 d0 03 …` | | 1 / 2 scroll | band−1, speed, v0 | V range = v0+off … v0+off+band, off −= / += speed per frame, wraps in the band. 7× type 1, 2× type 2 (rivers, lava: `1f 02 01`) | | 10 / 11 | band−1, speed, v0, n, n × u16 | scroll as 2 / 1, each wrap switches to the next / previous frame. Unused | | 7 / 9 | u16 palette, u16 delay, u8 first, u8 last | palette cycling of 8-bit pages. Unused (no palettes) | The editor viewport does not animate textures; the Animation tab previews them. ## Known limits * Texture entries are never deleted (indices are referenced by faces and object models); unused ones can be reused by moving their rectangle. * Object (STPC) model faces cannot be painted. * Painting a shared original tile model changes every tile using it unless **Unique** is on. * The page format of an existing page is not changed by the importer; importing into a page of another class is avoided. Files: `src/main/java/com/eliasmueller/llamamod/editor/tools/textures/*`, tests `TextureLibraryTest` (colour reduction, anim round trip of all village animations, packing, build + undo), tour steps `TexTourSteps` (with `playtest.EdTour`).