@poe2-toolkit/tree-react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vladislav Rajtmajer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,165 @@
1
+ # @poe2-toolkit/tree-react
2
+
3
+ [![npm](https://img.shields.io/npm/v/@poe2-toolkit/tree-react.svg)](https://www.npmjs.com/package/@poe2-toolkit/tree-react)
4
+ [![types: TypeScript](https://img.shields.io/badge/types-TypeScript-3178c6.svg)](src/index.ts)
5
+ [![React 18+](https://img.shields.io/badge/react-18%2B-61dafb.svg)](https://react.dev)
6
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+
8
+ React renderer for the Path of Exile 2 passive tree. It's a thin view layer on
9
+ top of [`@poe2-toolkit/tree-core`](../poe2-tree-core): the core works out where
10
+ everything goes, and this package draws it and runs the canvas, panning,
11
+ zooming, hovering, and clicking.
12
+
13
+ It does no geometry of its own. Positions, sizes, rotations, the hub layout, and
14
+ hit-testing all come from the core. If you ever catch this package computing a
15
+ coordinate, that's a bug.
16
+
17
+ The geometry lives in a framework-agnostic core, so the same `Scene` can just as
18
+ easily be drawn from Vue, Svelte, or anything else. React is what this project
19
+ happens to use.
20
+
21
+ > **Live demo:** see this renderer in a real app at
22
+ > [poe.rajtik.com/tree](https://poe.rajtik.com/tree).
23
+
24
+ ## Who does what
25
+
26
+ | Concern | Owner |
27
+ |---|---|
28
+ | node positions, sizes, hub geometry, arcs, hit-test math | core |
29
+ | canvas, device-pixel sizing, draw loop, layer order | this package |
30
+ | pan, zoom, wheel, fullscreen, pointer hover and click | this package |
31
+ | loading atlas bitmaps, colors, tooltips, surrounding UI | you |
32
+
33
+ ## Install
34
+
35
+ ```sh
36
+ npm install @poe2-toolkit/tree-react @poe2-toolkit/tree-core
37
+ ```
38
+
39
+ React 18 or newer is a peer dependency.
40
+
41
+ ## Usage
42
+
43
+ The tree data comes from GGG's official skill-tree export. Run it through the
44
+ core's GGG adapter to get a `TreeData`, build a `Scene` from it, and hand that to
45
+ `TreeView`.
46
+
47
+ ```tsx
48
+ import { buildScene } from '@poe2-toolkit/tree-core';
49
+ import { normalizeGggTree } from '@poe2-toolkit/tree-core/ggg';
50
+ import { TreeView } from '@poe2-toolkit/tree-react';
51
+
52
+ // Normalize GGG's data.json into the engine's TreeData (once per tree).
53
+ const data = normalizeGggTree(rawGggExport, '0_5');
54
+
55
+ // Build a render-ready scene for the current build (rebuild it on edits).
56
+ const scene = buildScene(data, { allocation });
57
+
58
+ // Draw it. `resources` (atlas bitmaps + manifest) is optional; leave it out
59
+ // and you get the vector debug render.
60
+ <TreeView
61
+ scene={scene}
62
+ resources={{ manifest, atlases }}
63
+ activeClassId={allocation.classId}
64
+ activeAscendancy={allocation.ascendId}
65
+ onNodeClick={(skill) => toggle(skill)}
66
+ />;
67
+ ```
68
+
69
+ The pattern is state in, intent out. The `scene` already holds everything
70
+ visual, so the component just reports what the user did (`onNodeClick`,
71
+ `onNodeHover`, and the rest) and never touches the build itself.
72
+
73
+ ## Graphics
74
+
75
+ `TreeView` doesn't load any images. You give it a `RenderResources`:
76
+
77
+ ```ts
78
+ interface RenderResources {
79
+ manifest: SpriteManifest; // sprite key -> native atlas rect
80
+ atlases: Record<string, CanvasImageSource>; // atlas id -> bitmap
81
+ }
82
+ ```
83
+
84
+ To draw a node, the renderer turns it into a sprite key with the helpers in
85
+ [`spriteKeys`](src/spriteKeys.ts) (`iconKeyFor`, `frameKeyFor`, `effectKeyFor`,
86
+ and friends), looks that key up in your `manifest`, and blits the rect from the
87
+ matching atlas. The keys follow GGG's atlas naming, so pointing the renderer at a
88
+ different atlas set comes down to swapping that one file. Leave `resources` out
89
+ and you get a plain vector render of discs and rails, which is handy for
90
+ debugging without art.
91
+
92
+ The hub artwork (class portrait and ornate ring) comes in through the optional
93
+ `centreSprites` prop. Skip it and the hub falls back to a vector placeholder.
94
+
95
+ ## Component props
96
+
97
+ ```tsx
98
+ <TreeView
99
+ scene={scene} // required: core.buildScene output
100
+ resources={resources} // atlas bitmaps + manifest (omit for vector)
101
+ activeClassId={classId} // rotates the active ring onto the class
102
+ activeAscendancy={ascId} // relocates that ascendancy disc into the hub
103
+ centreSprites={centreSprites} // optional portrait + ring artwork
104
+ preview={preview} // hover highlight: pending add (gold) / remove (red)
105
+ focus={worldRect} // pass a fresh rect to pan + zoom-fit to it
106
+ wheelZoom // turn on wheel zoom (off by default)
107
+ controls={controlsRef} // imperative zoomIn() / zoomOut()
108
+ onNodeClick={(skill, screen) => …}
109
+ onNodeDoubleClick={(skill) => …}
110
+ onNodeHover={(skill, screen) => …}
111
+ onInteractStart={() => …} // a press started on the canvas (e.g. close popovers)
112
+ />
113
+ ```
114
+
115
+ Exported types: `TreeViewProps`, `TreeViewControls`, `AllocationPreview`,
116
+ `CentreSprite`, `RenderResources`.
117
+
118
+ For external +/- buttons, reach for the imperative handle on `controls`:
119
+
120
+ ```tsx
121
+ const controls = useRef<TreeViewControls>(null);
122
+ // …
123
+ <button onClick={() => controls.current?.zoomIn()}>+</button>
124
+ ```
125
+
126
+ ## Non-goals
127
+
128
+ This package won't:
129
+
130
+ - compute or adjust any position, size, rotation, or hub placement;
131
+ - carry magic numbers for node, icon, or effect sizing;
132
+ - lock itself to one data source (it only knows `Scene` and `SpriteManifest`);
133
+ - claim to be the only frontend. A Vue, Svelte, or Livewire renderer on the same
134
+ contract is every bit as valid.
135
+
136
+ ## Local development
137
+
138
+ In-repo, this package finds `@poe2-toolkit/tree-core` two ways:
139
+
140
+ - typecheck and build read it through a `tsconfig` `paths` entry that points at
141
+ the sibling source, so there's no build or link step;
142
+ - `npm install` links the sibling through the `file:../poe2-tree-core`
143
+ dependency.
144
+
145
+ When you split this out into its own repo, change two things: drop the `paths`
146
+ block in `tsconfig.json`, and swap the `@poe2-toolkit/tree-core` dependency from
147
+ `file:../poe2-tree-core` to a published version like `^0.1.0`.
148
+
149
+ ```sh
150
+ npm install
151
+ npm run typecheck
152
+ npm run build
153
+ ```
154
+
155
+ ## Attributions and legal
156
+
157
+ This is an unofficial, fan-made project, **not** affiliated with, endorsed by, or
158
+ sponsored by Grinding Gear Games. "Path of Exile 2" is a trademark of Grinding
159
+ Gear Games, and all game content, data, and art are their property. This package
160
+ ships code only and stores nothing derived from the game. Thank you to Grinding
161
+ Gear Games for making Path of Exile 2. See the repository [NOTICE](../../NOTICE.md).
162
+
163
+ ## License
164
+
165
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,101 @@
1
+ import type { Scene, WorldRect } from '@poe2-toolkit/tree-core';
2
+ import type { CSSProperties, Ref } from 'react';
3
+ import type { RenderResources } from './resources.js';
4
+ export interface TreeViewProps {
5
+ /** Computed geometry from `@poe2-toolkit/tree-core`'s `buildScene`. */
6
+ scene: Scene;
7
+ /** Atlas bitmaps + manifest. Omit for the vector debug render (no GGG art). */
8
+ resources?: RenderResources;
9
+ /** Active class id — rotates the active ring onto that class. */
10
+ activeClassId?: number;
11
+ /** Active ascendancy id — its disc is relocated into the hub. */
12
+ activeAscendancy?: string;
13
+ /**
14
+ * Optional centre artwork. Each sprite is a URL + source sub-rect, drawn at
15
+ * the hub sized to the matching core radius:
16
+ * - `portrait` -> `ring.artRadius` (the inner class illustration)
17
+ * - `ringStatic` -> `ring.frameRadius` (the ornate ring)
18
+ * - `ringActive` -> `ring.activeRadius`, rotated by the active class's
19
+ * `ringRotation` (the gold band pointing at the class)
20
+ * When absent, the vector hub stand-in is drawn instead.
21
+ */
22
+ centreSprites?: {
23
+ portrait?: CentreSprite;
24
+ ringStatic?: CentreSprite;
25
+ ringActive?: CentreSprite;
26
+ };
27
+ /** Single-click intent out: the skill id and the node's centre in canvas px. */
28
+ onNodeClick?: (skill: number, screen: {
29
+ x: number;
30
+ y: number;
31
+ }) => void;
32
+ /** Double-click intent out (skill id). */
33
+ onNodeDoubleClick?: (skill: number) => void;
34
+ /** Fires when a press starts on the canvas (pan/empty click) — e.g. to dismiss popovers. */
35
+ onInteractStart?: () => void;
36
+ /**
37
+ * Hover feedback out: the skill id (or null when leaving all nodes) and, when
38
+ * hovering a node, its centre in canvas pixels — so callers can anchor UI
39
+ * (e.g. an attribute picker) to the node.
40
+ */
41
+ onNodeHover?: (skill: number | null, screen?: {
42
+ x: number;
43
+ y: number;
44
+ }) => void;
45
+ /**
46
+ * Hover preview to highlight: the nodes/edges that a click would allocate
47
+ * (`add`) or remove (`remove`). Drawn on top of the base render.
48
+ */
49
+ preview?: AllocationPreview | null;
50
+ /** Imperative zoom controls, for external +/- buttons. */
51
+ controls?: Ref<TreeViewControls>;
52
+ /**
53
+ * Enable mouse-wheel zoom. Off by default so the page can scroll over an
54
+ * embedded canvas; turn on in fullscreen, where there's nothing to scroll.
55
+ */
56
+ wheelZoom?: boolean;
57
+ /**
58
+ * World rect to frame (pan + zoom to fit). Whenever the object reference
59
+ * changes the view re-frames it — pass a fresh object to trigger, `null` to
60
+ * keep the current/default view (the hub).
61
+ */
62
+ focus?: WorldRect | null;
63
+ /**
64
+ * Skill ids to emphasise with a standing teal ring (e.g. name-search hits).
65
+ * Unlike `onNodeHover`, this is a persistent set drawn until it changes.
66
+ */
67
+ highlight?: Set<number> | null;
68
+ className?: string;
69
+ style?: CSSProperties;
70
+ }
71
+ /** Imperative handle exposed via `controls` for external zoom buttons. */
72
+ export interface TreeViewControls {
73
+ zoomIn: () => void;
74
+ zoomOut: () => void;
75
+ }
76
+ /** Hover preview of a pending allocate/remove: node ids + edge keys to glow. */
77
+ export interface AllocationPreview {
78
+ kind: 'add' | 'remove';
79
+ nodes: Set<number>;
80
+ /** Edge keys as `min-max` of the two node ids. */
81
+ edges: Set<string>;
82
+ }
83
+ /** A sprite to draw at the hub: source image URL + the sub-rect to crop. */
84
+ export interface CentreSprite {
85
+ url: string;
86
+ sx: number;
87
+ sy: number;
88
+ sw: number;
89
+ sh: number;
90
+ }
91
+ /**
92
+ * Thin canvas view over a core `Scene`. It owns nothing geometric — pan, zoom,
93
+ * device-pixel sizing, the draw loop, and hover hit-testing only. Positions,
94
+ * sizes, projection and hit-testing all come from `@poe2-toolkit/tree-core`.
95
+ *
96
+ * Without `resources` it renders a vector debug view (nodes as discs, edges as
97
+ * lines/arcs, the hub opening as a ring) — enough to see the geometry before any
98
+ * GGG atlas art exists.
99
+ */
100
+ export declare function TreeView({ scene, resources, activeClassId, activeAscendancy, centreSprites, onNodeClick, onNodeDoubleClick, onInteractStart, onNodeHover, preview, controls, wheelZoom, focus, highlight, className, style, }: TreeViewProps): React.JSX.Element;
101
+ //# sourceMappingURL=TreeView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TreeView.d.ts","sourceRoot":"","sources":["../src/TreeView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAY,KAAK,EAAqC,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAG7G,OAAO,KAAK,EAAE,aAAa,EAAoE,GAAG,EAAE,MAAM,OAAO,CAAC;AAClH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAGtD,MAAM,WAAW,aAAa;IAC5B,uEAAuE;IACvE,KAAK,EAAE,KAAK,CAAC;IACb,+EAA+E;IAC/E,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,iEAAiE;IACjE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iEAAiE;IACjE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE;QACd,QAAQ,CAAC,EAAE,YAAY,CAAC;QACxB,UAAU,CAAC,EAAE,YAAY,CAAC;QAC1B,UAAU,CAAC,EAAE,YAAY,CAAC;KAC3B,CAAC;IACF,gFAAgF;IAChF,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACxE,0CAA0C;IAC1C,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,4FAA4F;IAC5F,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;IAC7B;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAChF;;;OAGG;IACH,OAAO,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACnC,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACjC;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IACzB;;;OAGG;IACH,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAED,0EAA0E;AAC1E,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,gFAAgF;AAChF,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,KAAK,GAAG,QAAQ,CAAC;IACvB,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACnB,kDAAkD;IAClD,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACpB;AAED,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;CACZ;AA0ED;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,EACvB,KAAK,EACL,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,eAAe,EACf,WAAW,EACX,OAAO,EACP,QAAQ,EACR,SAAS,EACT,KAAK,EACL,SAAS,EACT,SAAS,EACT,KAAK,GACN,EAAE,aAAa,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAkWnC"}