TextureForge¶
Spec-driven texture studio for the game's painterly UI. Small JSON recipes render deterministic
brush-stroke plates, torn parchment panels, frames, dividers, bars and slots, exported as
production PNGs plus ready FSlateBrush metadata for the WidgetForge pipeline.
Lives in Tools/TextureForge/. Operational detail is co-located with the tool — this doc is the
concept map; Tools/TextureForge/CLAUDE.md (agent workflow, hard rules) and
Tools/TextureForge/README.md (studio workflow, recipe anatomy) are the sources of truth for
day-to-day use.
The one split that matters: recipes vs comps¶
Recipes (recipes/*.tex.json) |
Comps (comps/*.comp.json) |
|
|---|---|---|
| Purpose | Define ONE texture | Assemble a preview mockup (tooltip, panel, menu) |
| Output | Shippable PNG + brush metadata via npm run export |
Screenshot only |
| Ships to UE | Yes | Never |
Comps exist so a texture set is judged in context on #0a0a0a before anything touches UE. They can
reference fixtures (fixtures/*.fixtures.json — sample runtime data) for populated previews, but the
export path must never read fixtures — the determinism gate enforces this. If a comp looks right, the
production path is still: adjust the recipes → npm run export → WidgetForge spec. This is the
easiest thing to get wrong when new to the tool.
Recipes¶
{
"name": "T_UI_ButtonPlate_M",
"archetype": "button-plate",
"preset": "parchment-plate",
"size": { "w": 300, "h": 52 },
"seed": 1,
"slicing": { "mode": "fixed" },
"params": { "bleed": 0.2 },
"export": { "scales": [1, 2] }
}
- Archetypes (registry:
core/render.jsARCHETYPES):button-plate,tab-plate,pattern-band,row-highlight,divider,gradient,section-header,panel,slot-well,ornament,emblem,corner-bracket(+probe, a diagnostic). Icon recipes (T_UI_Icon_*) are generated from the icon manifest (icons/, compiled byexport/icons.mjs) — tune the manifest or the icon gallery, never hand-edit a generated recipe. - Param merge order (
effectiveParams,core/render.js): archetype defaults ⊕ preset ⊕recipe.params, last wins. Params are overrides on the merged base — the studio shows overridden values in gold. - Slicing:
fixed(baked plate at final size, S/M/L variants — brush character everywhere) ornine/three-h/three-vwithmargins(logical px). Sliced recipes confine organic tear detail to corner zones so Slate Box stretching can't smear it. Never confuse the two kinds. - Colors:
$tokenfromTools/WidgetForge/tokens.json(single palette source of truth) or#hex— but never hardcode a hex that exists as a token. - Overlays: any recipe stacks
"overlays": [...]over its base render, applied in array order. Types:pattern(tiled ornament with wear),seal(procedural ghost stamp),scratches,wash,image(external PNG fromoverlays/— the hand-paint escape hatch). Shared props on every layer:color,alpha,blend(normal/multiply/screen),tint,clip(paint only where the base has alpha),seedoffset.
Comp node language¶
Defined and documented in studio/comp-render.js (header comment is the node reference; the compose
page has a live reference drawer). It deliberately mirrors WidgetForge spec names — VerticalBox,
HorizontalBox, Overlay, TextBlock, Image, Border, Spacer, Canvas — with slots flattened
onto children, plus a forge-specific Texture node: sliced recipes 9-slice-stretch, fixed plates
re-render at the requested logical size (exactly what UE will do). TextBlock supports inline
colored spans. See Comp_TalentTooltip for a full example.
Invocations¶
From Tools/TextureForge/:
| Command | Does |
|---|---|
npm run dev |
Studio at http://localhost:4173 (texture editor; /studio/compose.html?comp=<Name> for comps; /studio/icons.html for the icon gallery) |
npm run export |
All recipes → exports/: PNGs @1x/@2x, manifest.json (FSlateBrush blocks), spec-snippets.json, import_forge_textures.py |
node export/export.mjs <name> |
Export one recipe |
node export/comp-shot.mjs [comp...] |
Comp screenshots → exports/comps/ |
node export/ingest.mjs <png> <name> |
Ingest a black-on-white icon stamp → overlays/icons/ (authoring spec: overlays/icons/README.md) |
npm run test:determinism |
Goldens gate (--comps adds fixtures-isolation + comp-shot goldens; --icons scopes to generated icons) |
npm run push [name...] |
Import into a running editor via the WidgetForge push watcher |
Textures land under /Game/TextureLibrary/UI/Forge/. Brush-block ImageSize is logical size —
@2x textures draw crisp at half pixel res.
The determinism gate¶
Same recipe + same seed = same pixels, forever — no Math.random() in the render path, seeded RNG
only. npm run test:determinism compares renders against committed goldens; any new or changed
recipe must pass it before landing. Icon tuning through the gallery's Save rebaselines its own
goldens (--icons --update), so it never needs a manual rebaseline; everything else that
intentionally changes pixels does.