@poe2-toolkit/tree-react
v1.0.0
Published
React renderer for the Path of Exile 2 passive tree. A thin view adapter over @poe2-toolkit/tree-core: it draws what the core computed and owns pan/zoom/hover/click. No geometry.
Maintainers
Readme
@poe2-toolkit/tree-react
React renderer for the Path of Exile 2 passive tree. It's a thin view layer on
top of @poe2-toolkit/tree-core: the core works out where
everything goes, and this package draws it on a WebGL canvas (via PixiJS) and
runs panning, zooming, hovering, and clicking.
The whole tree is built once as a Pixi scene graph in world space; pan and zoom
only move the world container's transform, so the GPU recomposites without
re-rasterising any sprites. That keeps a full-tree pan/zoom smooth even where a
Canvas2D drawImage would fall back to the CPU.
It does no geometry of its own. Positions, sizes, rotations, the hub layout, and hit-testing all come from the core. If you ever catch this package computing a coordinate, that's a bug.
The geometry lives in a framework-agnostic core, so the same Scene can just as
easily be drawn from Vue, Svelte, or anything else. React is what this project
happens to use.
Live demo: see this renderer in a real app at poe.rajtik.com/tree.
Who does what
| Concern | Owner | |---|---| | node positions, sizes, hub geometry, arcs, hit-test math | core | | WebGL canvas (PixiJS), device-pixel sizing, scene graph, layer order | this package | | pan, zoom, wheel, fullscreen, pointer hover and click | this package | | loading atlas bitmaps, colors, tooltips, surrounding UI | you |
Install
npm install @poe2-toolkit/tree-react @poe2-toolkit/tree-coreReact 18 or newer is a peer dependency. PixiJS (pixi.js v8) is a direct
dependency, pulled in automatically. It's the WebGL backend.
Usage
The tree data comes from GGG's official skill-tree export. Run it through the
core's GGG adapter to get a TreeData, build a Scene from it, and hand that to
TreeView.
import { buildScene } from '@poe2-toolkit/tree-core';
import { normalizeGggTree } from '@poe2-toolkit/tree-core/ggg';
import { TreeView } from '@poe2-toolkit/tree-react';
// Normalize GGG's data.json into the engine's TreeData (once per tree).
const data = normalizeGggTree(rawGggExport, '0_5');
// Build a render-ready scene for the current build (rebuild it on edits).
const scene = buildScene(data, { allocation });
// Draw it. `resources` (atlas bitmaps + manifest) is optional; leave it out
// and you get the vector debug render.
<TreeView
scene={scene}
resources={{ manifest, atlases }}
activeClassId={allocation.classId}
activeAscendancy={allocation.ascendId}
onNodeClick={(skill) => toggle(skill)}
/>;The pattern is state in, intent out. The scene already holds everything
visual, so the component just reports what the user did (onNodeClick,
onNodeHover, and the rest) and never touches the build itself.
Graphics
TreeView doesn't load any images. You give it a RenderResources:
interface RenderResources {
manifest: SpriteManifest; // sprite key -> native atlas rect
atlases: Record<string, CanvasImageSource>; // atlas id -> bitmap
}To draw a node, the renderer turns it into a sprite key with the helpers in
spriteKeys (iconKeyFor, frameKeyFor, effectKeyFor,
arcConnectorKeyFor, lineConnectorKeyFor), looks that key up in your
manifest, and blits the rect from the
matching atlas. The keys follow GGG's atlas naming, so pointing the renderer at a
different atlas set comes down to swapping that one file. Leave resources out
and you get a plain vector render of discs and rails, which is handy for
debugging without art.
A node has one colour icon. Unallocated nodes draw that same icon dimmed with a 50% grey multiply tint at draw time, the game's own look, so you supply a single skill-icon atlas, not a separate inactive one.
Weapon sets. When the scene's nodes/rails carry a weaponSet tag (set by
core.buildScene from the build's weaponSets), TreeView tints them apart
from the gold basic tree: set I red, set II green by default (the in-game
colours), on both the node frame and its active rail. A rail bridging the two
sets is drawn inactive, since it belongs to neither set's view. The set tints
and the removal-preview colour are tunable through the colors prop; the
defaults are also exported as DEFAULT_TREE_COLORS if you want to extend
rather than replace them.
The hub artwork (class portrait and ornate ring) comes in through the optional
centreSprites prop, holding up to three CentreSprites (portrait,
ringStatic, ringActive). Each is a source image url plus the sub-rect to
crop (sx, sy, sw, sh). Skip the prop and the hub falls back to a vector
placeholder.
Component props
<TreeView
scene={scene} // required: core.buildScene output
resources={resources} // atlas bitmaps + manifest (omit for vector)
activeClassId={classId} // rotates the active ring onto the class
activeAscendancy={ascId} // relocates that ascendancy disc into the hub
centreSprites={centreSprites} // optional portrait + ring artwork
preview={preview} // hover highlight: pending add (gold, or the set's tint) / remove (magenta, ringing each removed node)
edgeOverlays={overlays} // paint groups of edges, each in its own colour (a diff, a route, a plan)
highlight={searchHits} // skill ids to ring with a standing teal ring (e.g. search hits)
highlightStyle={ringStyle} // tune the highlight rings' colours, widths and pulse
colors={palette} // tune the weapon-set tints + removal-preview colour (defaults: set I red, set II green, removal magenta)
focus={worldRect} // pass a fresh rect to pan + zoom-fit to it
wheelZoom // turn on wheel zoom (off by default)
debugIds // overlay each node's skill id (debug; off by default)
zoom={{ maxScale, minFitFactor, overscroll }} // tune zoom-in cap, zoom-out floor, pan slack
controls={controlsRef} // imperative zoomIn() / zoomOut()
onNodeClick={(skill, screen) => …}
onNodeDoubleClick={(skill) => …}
onNodeHover={(skill, screen) => …}
onInteractStart={() => …} // a press started on the canvas (e.g. close popovers)
className={className} // forwarded to the canvas wrapper element
style={style} // forwarded to the canvas wrapper element
/>Exported types: TreeViewProps, TreeViewControls, AllocationPreview,
EdgeOverlay, CentreSprite, ZoomLimits, HighlightStyle, RenderResources.
Every prop and every field on these types carries its own doc-comment, shown on
hover in your editor and shipped in the package's .d.ts — so the per-field
detail lives there, not duplicated here.
preview is an AllocationPreview: kind (add or remove), the nodes
(skill ids) and edges (edge keys, min-max of the two node ids) it would
touch, and an optional weaponSet (1 or 2) that tints an add into that set's
colour. Pass null to clear it.
edgeOverlays paints arbitrary groups of edges over the base render, each in its
own colour. It is a multi-colour generalisation of preview's edge highlight,
and the renderer stays agnostic about what a group means: you decide, whether a
build diff, a planned route, or anything else. Each group is { edges, color,
includeInactive? }, where edges is a set of edge keys (min-max of the two
node ids), color is 0xRRGGBB, and includeInactive also paints edges whose
endpoints are not both allocated (the dim base rails), so you can surface a path
the build does not yet hold. Groups are drawn in array order, so a later one wins
where two share an edge.
highlightStyle tunes the look of the highlight rings, each field optional:
glowColor / coreColor (the soft outer and bright inner ring), glowWidth /
coreWidth, radius (gap from the node frame), pulseMs (period; 0 for a still
ring), pulseGrow (extra radius at the peak), and glowAlpha / coreAlpha
([trough, peak]). Omit the prop for the standing teal pulse.
zoom tunes the view extents, each field optional: maxScale caps zoom-in,
minFitFactor is the zoom-out floor as a multiple of the fit-the-whole-tree
scale (1 stops you zooming out past the whole tree), and overscroll is how far
past the edges you can pan. Omit zoom for the defaults (4, 0.85, 0.5).
For external +/- buttons, reach for the imperative handle on controls:
const controls = useRef<TreeViewControls>(null);
// …
<button onClick={() => controls.current?.zoomIn()}>+</button>Non-goals
This package won't:
- compute or adjust any position, size, rotation, or hub placement;
- carry magic numbers for node, icon, or effect sizing;
- lock itself to one data source (it only knows
SceneandSpriteManifest); - claim to be the only frontend. A Vue, Svelte, or Livewire renderer on the same contract is every bit as valid.
Attributions and legal
This is an unofficial, fan-made project, not affiliated with, endorsed by, or sponsored by Grinding Gear Games. "Path of Exile 2" is a trademark of Grinding Gear Games, and all game content, data, and art are their property. This package ships code only and stores nothing derived from the game. Thank you to Grinding Gear Games for making Path of Exile 2. See the repository NOTICE.
License
MIT. See LICENSE.
