@xenolithengine/graph-render-pixi 0.7.0-beta.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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -0
  3. package/dist/bezier.d.ts +30 -0
  4. package/dist/bezier.d.ts.map +1 -0
  5. package/dist/bezier.js +70 -0
  6. package/dist/bezier.js.map +1 -0
  7. package/dist/comment-renderer.d.ts +35 -0
  8. package/dist/comment-renderer.d.ts.map +1 -0
  9. package/dist/comment-renderer.js +194 -0
  10. package/dist/comment-renderer.js.map +1 -0
  11. package/dist/edge-paths.d.ts +24 -0
  12. package/dist/edge-paths.d.ts.map +1 -0
  13. package/dist/edge-paths.js +123 -0
  14. package/dist/edge-paths.js.map +1 -0
  15. package/dist/edge-renderer.d.ts +29 -0
  16. package/dist/edge-renderer.d.ts.map +1 -0
  17. package/dist/edge-renderer.js +99 -0
  18. package/dist/edge-renderer.js.map +1 -0
  19. package/dist/geom.d.ts +46 -0
  20. package/dist/geom.d.ts.map +1 -0
  21. package/dist/geom.js +54 -0
  22. package/dist/geom.js.map +1 -0
  23. package/dist/grid.d.ts +12 -0
  24. package/dist/grid.d.ts.map +1 -0
  25. package/dist/grid.js +35 -0
  26. package/dist/grid.js.map +1 -0
  27. package/dist/icons.d.ts +14 -0
  28. package/dist/icons.d.ts.map +1 -0
  29. package/dist/icons.js +32 -0
  30. package/dist/icons.js.map +1 -0
  31. package/dist/index.d.ts +34 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +20 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/interaction.d.ts +69 -0
  36. package/dist/interaction.d.ts.map +1 -0
  37. package/dist/interaction.js +229 -0
  38. package/dist/interaction.js.map +1 -0
  39. package/dist/layout.d.ts +126 -0
  40. package/dist/layout.d.ts.map +1 -0
  41. package/dist/layout.js +360 -0
  42. package/dist/layout.js.map +1 -0
  43. package/dist/macro-frame.d.ts +29 -0
  44. package/dist/macro-frame.d.ts.map +1 -0
  45. package/dist/macro-frame.js +116 -0
  46. package/dist/macro-frame.js.map +1 -0
  47. package/dist/node-renderer.d.ts +102 -0
  48. package/dist/node-renderer.d.ts.map +1 -0
  49. package/dist/node-renderer.js +776 -0
  50. package/dist/node-renderer.js.map +1 -0
  51. package/dist/reroute-renderer.d.ts +25 -0
  52. package/dist/reroute-renderer.d.ts.map +1 -0
  53. package/dist/reroute-renderer.js +163 -0
  54. package/dist/reroute-renderer.js.map +1 -0
  55. package/dist/style.d.ts +56 -0
  56. package/dist/style.d.ts.map +1 -0
  57. package/dist/style.js +102 -0
  58. package/dist/style.js.map +1 -0
  59. package/dist/text-measure.d.ts +6 -0
  60. package/dist/text-measure.d.ts.map +1 -0
  61. package/dist/text-measure.js +21 -0
  62. package/dist/text-measure.js.map +1 -0
  63. package/dist/theme.d.ts +86 -0
  64. package/dist/theme.d.ts.map +1 -0
  65. package/dist/theme.js +2 -0
  66. package/dist/theme.js.map +1 -0
  67. package/dist/viewport-math.d.ts +68 -0
  68. package/dist/viewport-math.d.ts.map +1 -0
  69. package/dist/viewport-math.js +106 -0
  70. package/dist/viewport-math.js.map +1 -0
  71. package/dist/viewport.d.ts +28 -0
  72. package/dist/viewport.d.ts.map +1 -0
  73. package/dist/viewport.js +47 -0
  74. package/dist/viewport.js.map +1 -0
  75. package/dist/virtualize.d.ts +55 -0
  76. package/dist/virtualize.d.ts.map +1 -0
  77. package/dist/virtualize.js +96 -0
  78. package/dist/virtualize.js.map +1 -0
  79. package/dist/widget-renderer.d.ts +150 -0
  80. package/dist/widget-renderer.d.ts.map +1 -0
  81. package/dist/widget-renderer.js +399 -0
  82. package/dist/widget-renderer.js.map +1 -0
  83. package/dist/xen-theme.d.ts +8 -0
  84. package/dist/xen-theme.d.ts.map +1 -0
  85. package/dist/xen-theme.js +23 -0
  86. package/dist/xen-theme.js.map +1 -0
  87. package/package.json +63 -0
@@ -0,0 +1,68 @@
1
+ export interface Vec2 {
2
+ x: number;
3
+ y: number;
4
+ }
5
+ export interface ViewportState {
6
+ /** World origin x-position in screen coordinates. */
7
+ x: number;
8
+ /** World origin y-position in screen coordinates. */
9
+ y: number;
10
+ /** Scale factor: 1 = 1:1, 2 = zoomed in 2× (each world unit takes 2 screen pixels). */
11
+ zoom: number;
12
+ }
13
+ export type ZoomBounds = readonly [min: number, max: number];
14
+ export declare function screenToWorld(screen: Vec2, v: ViewportState): Vec2;
15
+ export declare function worldToScreen(world: Vec2, v: ViewportState): Vec2;
16
+ export declare function clampZoom(zoom: number, min: number, max: number): number;
17
+ /**
18
+ * Focal zoom: scale by `factor` while keeping the world point currently under `focalScreen`
19
+ * anchored to that same screen point. The classic UE / Figma / VSCode-canvas zoom feel.
20
+ *
21
+ * world_after = (screen - x') / zoom'
22
+ * require: world_after === world_before for screen === focalScreen
23
+ * solving: x' = focalScreen - world_before * zoom'
24
+ */
25
+ export declare function zoomAt(v: ViewportState, focalScreen: Vec2, factor: number, bounds?: ZoomBounds): ViewportState;
26
+ export interface Rect {
27
+ x: number;
28
+ y: number;
29
+ width: number;
30
+ height: number;
31
+ }
32
+ export interface FitViewOptions {
33
+ /** Screen-space margin (px) kept clear on every side. Default 64. */
34
+ padding?: number;
35
+ /** Never zoom in past this level — keeps tiny graphs from filling the screen. Default 1. */
36
+ maxZoom?: number;
37
+ /** Never zoom out past this level. Default 0.02. */
38
+ minZoom?: number;
39
+ }
40
+ /**
41
+ * Compute the viewport that frames a world-space bounds rect centred inside a screen of the given
42
+ * size, leaving `padding` px clear on each edge. The zoom fits the more-constraining axis; the
43
+ * content centre is mapped to the screen centre. Zero-size bounds are handled without div-by-zero.
44
+ */
45
+ export declare function fitView(bounds: Rect, screen: {
46
+ width: number;
47
+ height: number;
48
+ }, opts?: FitViewOptions): ViewportState;
49
+ export declare function snapToGrid(point: Vec2, cellSize: number): Vec2;
50
+ /**
51
+ * Resolve a node's final position during/at the end of a drag gesture.
52
+ *
53
+ * @param initial position when the drag started
54
+ * @param delta world-space delta accumulated since the start
55
+ * @param snap grid cell size, or null to disable snap (used while Alt is held)
56
+ */
57
+ export declare function computeDragTarget(initial: Vec2, delta: Vec2, snap: number | null): Vec2;
58
+ /**
59
+ * Resolve the uniform world-delta applied to *every* node in a multi-selection drag.
60
+ *
61
+ * Anchoring the snap to one node (the one under the cursor when the drag started) and applying
62
+ * the resulting delta to the rest preserves the group's internal layout. Snapping each node
63
+ * independently — what we did before — produced per-node rounding errors when nodes started
64
+ * off-grid: relative offsets drifted by up to a cell, so one node visibly "stuttered" out of
65
+ * formation. See drag-group regression test.
66
+ */
67
+ export declare function computeGroupSnappedDelta(anchorInitial: Vec2, rawDelta: Vec2, snap: number | null): Vec2;
68
+ //# sourceMappingURL=viewport-math.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewport-math.d.ts","sourceRoot":"","sources":["../src/viewport-math.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,IAAI;IACnB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV;AAED,MAAM,WAAW,aAAa;IAC5B,qDAAqD;IACrD,CAAC,EAAE,MAAM,CAAA;IACT,qDAAqD;IACrD,CAAC,EAAE,MAAM,CAAA;IACT,uFAAuF;IACvF,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,MAAM,UAAU,GAAG,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,CAAA;AAI5D,wBAAgB,aAAa,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,aAAa,GAAG,IAAI,CAKlE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,EAAE,aAAa,GAAG,IAAI,CAKjE;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAIxE;AAED;;;;;;;GAOG;AACH,wBAAgB,MAAM,CACpB,CAAC,EAAE,aAAa,EAChB,WAAW,EAAE,IAAI,EACjB,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,UAA2B,GAClC,aAAa,CAQf;AAED,MAAM,WAAW,IAAI;IACnB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,4FAA4F;IAC5F,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CACrB,MAAM,EAAE,IAAI,EACZ,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACzC,IAAI,GAAE,cAAmB,GACxB,aAAa,CAqBf;AAaD,wBAAgB,UAAU,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAQ9D;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAGvF;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,aAAa,EAAE,IAAI,EACnB,QAAQ,EAAE,IAAI,EACd,IAAI,EAAE,MAAM,GAAG,IAAI,GAClB,IAAI,CAON"}
@@ -0,0 +1,106 @@
1
+ const DEFAULT_BOUNDS = [0.05, 16];
2
+ export function screenToWorld(screen, v) {
3
+ return {
4
+ x: (screen.x - v.x) / v.zoom,
5
+ y: (screen.y - v.y) / v.zoom,
6
+ };
7
+ }
8
+ export function worldToScreen(world, v) {
9
+ return {
10
+ x: world.x * v.zoom + v.x,
11
+ y: world.y * v.zoom + v.y,
12
+ };
13
+ }
14
+ export function clampZoom(zoom, min, max) {
15
+ if (zoom < min)
16
+ return min;
17
+ if (zoom > max)
18
+ return max;
19
+ return zoom;
20
+ }
21
+ /**
22
+ * Focal zoom: scale by `factor` while keeping the world point currently under `focalScreen`
23
+ * anchored to that same screen point. The classic UE / Figma / VSCode-canvas zoom feel.
24
+ *
25
+ * world_after = (screen - x') / zoom'
26
+ * require: world_after === world_before for screen === focalScreen
27
+ * solving: x' = focalScreen - world_before * zoom'
28
+ */
29
+ export function zoomAt(v, focalScreen, factor, bounds = DEFAULT_BOUNDS) {
30
+ const targetZoom = clampZoom(v.zoom * factor, bounds[0], bounds[1]);
31
+ const worldFocal = screenToWorld(focalScreen, v);
32
+ return {
33
+ zoom: targetZoom,
34
+ x: focalScreen.x - worldFocal.x * targetZoom,
35
+ y: focalScreen.y - worldFocal.y * targetZoom,
36
+ };
37
+ }
38
+ /**
39
+ * Compute the viewport that frames a world-space bounds rect centred inside a screen of the given
40
+ * size, leaving `padding` px clear on each edge. The zoom fits the more-constraining axis; the
41
+ * content centre is mapped to the screen centre. Zero-size bounds are handled without div-by-zero.
42
+ */
43
+ export function fitView(bounds, screen, opts = {}) {
44
+ const padding = opts.padding ?? 64;
45
+ const maxZoom = opts.maxZoom ?? 1;
46
+ const minZoom = opts.minZoom ?? 0.02;
47
+ const availW = Math.max(1, screen.width - padding * 2);
48
+ const availH = Math.max(1, screen.height - padding * 2);
49
+ const fitZoom = bounds.width > 0 && bounds.height > 0
50
+ ? Math.min(availW / bounds.width, availH / bounds.height)
51
+ : maxZoom;
52
+ const zoom = clampZoom(fitZoom, minZoom, maxZoom);
53
+ const cx = bounds.x + bounds.width / 2;
54
+ const cy = bounds.y + bounds.height / 2;
55
+ return {
56
+ zoom,
57
+ x: screen.width / 2 - cx * zoom,
58
+ y: screen.height / 2 - cy * zoom,
59
+ };
60
+ }
61
+ /**
62
+ * Snap a point to the nearest multiple of `cellSize` on each axis. Used by drag to give the
63
+ * UE-style "feels continuous but actually quantised" feel. `cellSize === 1` rounds to integer
64
+ * pixels; pass `Number.EPSILON` (or skip the call) for true continuous motion.
65
+ */
66
+ /** Round-half-away-from-zero so snap is symmetric: 2.5 → 3, −2.5 → −3 (JS Math.round goes 2.5 → 3
67
+ * but −2.5 → −2, which makes drag feel jerky around the origin). */
68
+ function roundAwayFromZero(n) {
69
+ return n >= 0 ? Math.round(n) : -Math.round(-n);
70
+ }
71
+ export function snapToGrid(point, cellSize) {
72
+ if (!(cellSize > 0)) {
73
+ throw new Error(`snapToGrid: cellSize must be > 0, got ${cellSize}`);
74
+ }
75
+ const sx = roundAwayFromZero(point.x / cellSize) * cellSize;
76
+ const sy = roundAwayFromZero(point.y / cellSize) * cellSize;
77
+ // Normalise -0 to +0 so callers don't trip on Object.is(-0, 0) === false.
78
+ return { x: sx === 0 ? 0 : sx, y: sy === 0 ? 0 : sy };
79
+ }
80
+ /**
81
+ * Resolve a node's final position during/at the end of a drag gesture.
82
+ *
83
+ * @param initial position when the drag started
84
+ * @param delta world-space delta accumulated since the start
85
+ * @param snap grid cell size, or null to disable snap (used while Alt is held)
86
+ */
87
+ export function computeDragTarget(initial, delta, snap) {
88
+ const raw = { x: initial.x + delta.x, y: initial.y + delta.y };
89
+ return snap === null ? raw : snapToGrid(raw, snap);
90
+ }
91
+ /**
92
+ * Resolve the uniform world-delta applied to *every* node in a multi-selection drag.
93
+ *
94
+ * Anchoring the snap to one node (the one under the cursor when the drag started) and applying
95
+ * the resulting delta to the rest preserves the group's internal layout. Snapping each node
96
+ * independently — what we did before — produced per-node rounding errors when nodes started
97
+ * off-grid: relative offsets drifted by up to a cell, so one node visibly "stuttered" out of
98
+ * formation. See drag-group regression test.
99
+ */
100
+ export function computeGroupSnappedDelta(anchorInitial, rawDelta, snap) {
101
+ if (snap === null)
102
+ return rawDelta;
103
+ const anchorTarget = snapToGrid({ x: anchorInitial.x + rawDelta.x, y: anchorInitial.y + rawDelta.y }, snap);
104
+ return { x: anchorTarget.x - anchorInitial.x, y: anchorTarget.y - anchorInitial.y };
105
+ }
106
+ //# sourceMappingURL=viewport-math.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewport-math.js","sourceRoot":"","sources":["../src/viewport-math.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import { type Unsubscribe } from '@xenolithengine/graph-core';
2
+ import { type Vec2, type ViewportState, type ZoomBounds } from './viewport-math.js';
3
+ /**
4
+ * Minimal shape of the object Viewport applies state to. PIXI Container satisfies this — we only
5
+ * touch `scale.{x,y}` and `position.{x,y}`. Keeping the abstraction tiny lets us unit-test the
6
+ * Viewport without a WebGL context (any plain object with `scale`/`position` works).
7
+ */
8
+ export interface ApplyTarget {
9
+ scale: {
10
+ x: number;
11
+ y: number;
12
+ };
13
+ position: {
14
+ x: number;
15
+ y: number;
16
+ };
17
+ }
18
+ export declare class Viewport {
19
+ #private;
20
+ constructor(target: ApplyTarget, initial?: ViewportState);
21
+ get state(): ViewportState;
22
+ setState(state: ViewportState): void;
23
+ pan(dx: number, dy: number): void;
24
+ zoomAt(focal: Vec2, factor: number, bounds?: ZoomBounds): void;
25
+ reset(): void;
26
+ on(handler: (state: ViewportState) => void): Unsubscribe;
27
+ }
28
+ //# sourceMappingURL=viewport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewport.d.ts","sourceRoot":"","sources":["../src/viewport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,WAAW,EAAE,MAAM,4BAA4B,CAAA;AAC3E,OAAO,EAEL,KAAK,IAAI,EACT,KAAK,aAAa,EAClB,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAA;AAE3B;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC/B,QAAQ,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CACnC;AAQD,qBAAa,QAAQ;;gBAKP,MAAM,EAAE,WAAW,EAAE,OAAO,GAAE,aAA6B;IAMvE,IAAI,KAAK,IAAI,aAAa,CAEzB;IAED,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAapC,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAKjC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,UAAU,GAAG,IAAI;IAI9D,KAAK,IAAI,IAAI;IAIb,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAAG,WAAW;CAUzD"}
@@ -0,0 +1,47 @@
1
+ import { EventEmitter } from '@xenolithengine/graph-core';
2
+ import { zoomAt as zoomAtMath, } from './viewport-math.js';
3
+ const DEFAULT_STATE = { x: 0, y: 0, zoom: 1 };
4
+ export class Viewport {
5
+ #target;
6
+ #events = new EventEmitter();
7
+ #state;
8
+ constructor(target, initial = DEFAULT_STATE) {
9
+ this.#target = target;
10
+ this.#state = { ...initial };
11
+ this.#apply();
12
+ }
13
+ get state() {
14
+ return { ...this.#state };
15
+ }
16
+ setState(state) {
17
+ if (this.#state.x === state.x &&
18
+ this.#state.y === state.y &&
19
+ this.#state.zoom === state.zoom) {
20
+ return;
21
+ }
22
+ this.#state = { ...state };
23
+ this.#apply();
24
+ this.#events.emit('viewport:changed', this.state);
25
+ }
26
+ pan(dx, dy) {
27
+ if (dx === 0 && dy === 0)
28
+ return;
29
+ this.setState({ ...this.#state, x: this.#state.x + dx, y: this.#state.y + dy });
30
+ }
31
+ zoomAt(focal, factor, bounds) {
32
+ this.setState(zoomAtMath(this.#state, focal, factor, bounds));
33
+ }
34
+ reset() {
35
+ this.setState(DEFAULT_STATE);
36
+ }
37
+ on(handler) {
38
+ return this.#events.on('viewport:changed', handler);
39
+ }
40
+ #apply() {
41
+ this.#target.scale.x = this.#state.zoom;
42
+ this.#target.scale.y = this.#state.zoom;
43
+ this.#target.position.x = this.#state.x;
44
+ this.#target.position.y = this.#state.y;
45
+ }
46
+ }
47
+ //# sourceMappingURL=viewport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewport.js","sourceRoot":"","sources":["../src/viewport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAoB,MAAM,4BAA4B,CAAA;AAC3E,OAAO,EACL,MAAM,IAAI,UAAU,GAIrB,MAAM,oBAAoB,CAAA;AAY3B,MAAM,aAAa,GAAkB,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAA;AAM5D,MAAM,OAAO,QAAQ;IACV,OAAO,CAAa;IACpB,OAAO,GAAG,IAAI,YAAY,EAAU,CAAA;IAC7C,MAAM,CAAe;IAErB,YAAY,MAAmB,EAAE,UAAyB,aAAa;QACrE,IAAI,CAAC,OAAO,GAAG,MAAM,CAAA;QACrB,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,OAAO,EAAE,CAAA;QAC5B,IAAI,CAAC,MAAM,EAAE,CAAA;IACf,CAAC;IAED,IAAI,KAAK;QACP,OAAO,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,CAAA;IAC3B,CAAC;IAED,QAAQ,CAAC,KAAoB;QAC3B,IACE,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC;YACzB,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC;YACzB,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,EAC/B,CAAC;YACD,OAAM;QACR,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,KAAK,EAAE,CAAA;QAC1B,IAAI,CAAC,MAAM,EAAE,CAAA;QACb,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,kBAAkB,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;IACnD,CAAC;IAED,GAAG,CAAC,EAAU,EAAE,EAAU;QACxB,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC;YAAE,OAAM;QAChC,IAAI,CAAC,QAAQ,CAAC,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;IACjF,CAAC;IAED,MAAM,CAAC,KAAW,EAAE,MAAc,EAAE,MAAmB;QACrD,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC/D,CAAC;IAED,KAAK;QACH,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAA;IAC9B,CAAC;IAED,EAAE,CAAC,OAAuC;QACxC,OAAO,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAA;IACrD,CAAC;IAED,MAAM;QACJ,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAA;QACvC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAA;QACvC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;QACvC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;IACzC,CAAC;CACF"}
@@ -0,0 +1,55 @@
1
+ import type { Rect } from './geom.js';
2
+ import { type ViewportState } from './viewport-math.js';
3
+ /** Virtualization only kicks in past the threshold — at or below it, the graph renders 1:1 (zero
4
+ * risk for ordinary graphs). */
5
+ export declare function shouldVirtualize(nodeCount: number, threshold: number): boolean;
6
+ /**
7
+ * Whether to draw nodes in level-of-detail (simplified) form at the current zoom, with hysteresis:
8
+ * enter LOD below `enter`, leave it only above `exit` (exit > enter). The band between the two stops
9
+ * the detail level flip-flopping when the zoom sits right at the threshold during a gesture.
10
+ */
11
+ export declare function useLOD(zoom: number, currentlyLOD: boolean, enter: number, exit: number): boolean;
12
+ export type LODLevel = 'full' | 'sprite' | 'flat';
13
+ export interface LODThresholds {
14
+ /** flat↔sprite boundary: enter flat below lowEnter, leave flat above lowExit (lowExit > lowEnter). */
15
+ lowEnter: number;
16
+ lowExit: number;
17
+ /** sprite↔full boundary: enter full above highExit, leave full below highEnter (highExit > highEnter). */
18
+ highEnter: number;
19
+ highExit: number;
20
+ }
21
+ /**
22
+ * Three-tier level of detail with independent hysteresis on each boundary:
23
+ * - close zoom → 'full' (real nodes)
24
+ * - mid zoom → 'sprite' (one baked texture per node TYPE — recognisable, cheap, no LG overlap RTs)
25
+ * - far zoom → 'flat' (single batch of rects)
26
+ * `current` is the level in effect, used to apply hysteresis at whichever boundary the zoom is near.
27
+ */
28
+ export declare function lodLevel(zoom: number, current: LODLevel, t: LODThresholds): LODLevel;
29
+ /** World-space AABB of the screen viewport, inflated by `marginPx` screen pixels on every side
30
+ * (the overscan buffer, expressed in screen px so it stays constant regardless of zoom). */
31
+ export declare function visibleWorldRect(screenW: number, screenH: number, viewport: ViewportState, marginPx: number): Rect;
32
+ /** Bucket a world point into a grid cell key at the given cell size. */
33
+ export declare function cellKey(x: number, y: number, cell: number): string;
34
+ /** Every grid-cell key a world rect overlaps — used to gather only the nodes near the viewport
35
+ * instead of scanning the whole graph each frame (the O(N)-per-frame trap on huge graphs). */
36
+ export declare function cellsForRect(rect: Rect, cell: number): string[];
37
+ export interface Reconciliation {
38
+ /** Non-live nodes that entered the inner rect — create a view for these. */
39
+ show: string[];
40
+ /** Live nodes that left the outer rect — destroy their view (back to data only). */
41
+ hide: string[];
42
+ }
43
+ /**
44
+ * Decide which node views to create / destroy this pass, with hysteresis.
45
+ *
46
+ * - A node intersecting `inner` and not yet live → show (create).
47
+ * - A live node no longer intersecting `outer` → hide (destroy).
48
+ * - Anything in the band between (live, outside inner but inside outer) is left untouched.
49
+ *
50
+ * `rects` are world-space node AABBs; `live` is the set of node ids that currently have a view.
51
+ */
52
+ export declare function reconcileVisibleNodes(rects: ReadonlyArray<{
53
+ id: string;
54
+ } & Rect>, live: ReadonlySet<string>, inner: Rect, outer: Rect): Reconciliation;
55
+ //# sourceMappingURL=virtualize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"virtualize.d.ts","sourceRoot":"","sources":["../src/virtualize.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAErC,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAEtE;iCACiC;AACjC,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAE9E;AAED;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAGhG;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEjD,MAAM,WAAW,aAAa;IAC5B,sGAAsG;IACtG,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,0GAA0G;IAC1G,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,aAAa,GAAG,QAAQ,CAMpF;AAED;6FAC6F;AAC7F,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,aAAa,EACvB,QAAQ,EAAE,MAAM,GACf,IAAI,CASN;AAED,wEAAwE;AACxE,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;+FAC+F;AAC/F,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAQ/D;AAED,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,oFAAoF;IACpF,IAAI,EAAE,MAAM,EAAE,CAAA;CACf;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,aAAa,CAAC;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,EAC3C,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,EACzB,KAAK,EAAE,IAAI,EACX,KAAK,EAAE,IAAI,GACV,cAAc,CAYhB"}
@@ -0,0 +1,96 @@
1
+ // Viewport virtualization (#59): keep a live PIXI view only for nodes near the viewport, so GPU
2
+ // memory is O(visible) instead of O(total) — the fix for the ~5000-node crash. These are the PURE
3
+ // pieces (no PIXI, fully unit-tested); the editor wires them to its #views map and viewport changes.
4
+ //
5
+ // Hysteresis is the key to smoothness: a node is CREATED when it reaches the inner (overscan)
6
+ // rect, but only DESTROYED when it leaves the larger outer rect. The band between them means a node
7
+ // hovering at the edge during a pan is not churned create/destroy every frame — the thing that would
8
+ // cause flicker and stutter.
9
+ import { rectIntersects } from './geom.js';
10
+ import { screenToWorld } from './viewport-math.js';
11
+ /** Virtualization only kicks in past the threshold — at or below it, the graph renders 1:1 (zero
12
+ * risk for ordinary graphs). */
13
+ export function shouldVirtualize(nodeCount, threshold) {
14
+ return nodeCount > threshold;
15
+ }
16
+ /**
17
+ * Whether to draw nodes in level-of-detail (simplified) form at the current zoom, with hysteresis:
18
+ * enter LOD below `enter`, leave it only above `exit` (exit > enter). The band between the two stops
19
+ * the detail level flip-flopping when the zoom sits right at the threshold during a gesture.
20
+ */
21
+ export function useLOD(zoom, currentlyLOD, enter, exit) {
22
+ if (currentlyLOD)
23
+ return zoom < exit;
24
+ return zoom < enter;
25
+ }
26
+ /**
27
+ * Three-tier level of detail with independent hysteresis on each boundary:
28
+ * - close zoom → 'full' (real nodes)
29
+ * - mid zoom → 'sprite' (one baked texture per node TYPE — recognisable, cheap, no LG overlap RTs)
30
+ * - far zoom → 'flat' (single batch of rects)
31
+ * `current` is the level in effect, used to apply hysteresis at whichever boundary the zoom is near.
32
+ */
33
+ export function lodLevel(zoom, current, t) {
34
+ const flat = current === 'flat' ? zoom < t.lowExit : zoom < t.lowEnter;
35
+ if (flat)
36
+ return 'flat';
37
+ const full = current === 'full' ? zoom > t.highEnter : zoom > t.highExit;
38
+ if (full)
39
+ return 'full';
40
+ return 'sprite';
41
+ }
42
+ /** World-space AABB of the screen viewport, inflated by `marginPx` screen pixels on every side
43
+ * (the overscan buffer, expressed in screen px so it stays constant regardless of zoom). */
44
+ export function visibleWorldRect(screenW, screenH, viewport, marginPx) {
45
+ const topLeft = screenToWorld({ x: -marginPx, y: -marginPx }, viewport);
46
+ const bottomRight = screenToWorld({ x: screenW + marginPx, y: screenH + marginPx }, viewport);
47
+ return {
48
+ x: topLeft.x + 0, // normalise -0 → 0
49
+ y: topLeft.y + 0,
50
+ width: bottomRight.x - topLeft.x,
51
+ height: bottomRight.y - topLeft.y,
52
+ };
53
+ }
54
+ /** Bucket a world point into a grid cell key at the given cell size. */
55
+ export function cellKey(x, y, cell) {
56
+ return `${Math.floor(x / cell)},${Math.floor(y / cell)}`;
57
+ }
58
+ /** Every grid-cell key a world rect overlaps — used to gather only the nodes near the viewport
59
+ * instead of scanning the whole graph each frame (the O(N)-per-frame trap on huge graphs). */
60
+ export function cellsForRect(rect, cell) {
61
+ const x0 = Math.floor(rect.x / cell);
62
+ const y0 = Math.floor(rect.y / cell);
63
+ const x1 = Math.floor((rect.x + rect.width) / cell);
64
+ const y1 = Math.floor((rect.y + rect.height) / cell);
65
+ const keys = [];
66
+ for (let cx = x0; cx <= x1; cx++)
67
+ for (let cy = y0; cy <= y1; cy++)
68
+ keys.push(`${cx},${cy}`);
69
+ return keys;
70
+ }
71
+ /**
72
+ * Decide which node views to create / destroy this pass, with hysteresis.
73
+ *
74
+ * - A node intersecting `inner` and not yet live → show (create).
75
+ * - A live node no longer intersecting `outer` → hide (destroy).
76
+ * - Anything in the band between (live, outside inner but inside outer) is left untouched.
77
+ *
78
+ * `rects` are world-space node AABBs; `live` is the set of node ids that currently have a view.
79
+ */
80
+ export function reconcileVisibleNodes(rects, live, inner, outer) {
81
+ const show = [];
82
+ const hide = [];
83
+ for (const r of rects) {
84
+ const isLive = live.has(r.id);
85
+ if (!isLive) {
86
+ if (rectIntersects(r, inner))
87
+ show.push(r.id);
88
+ }
89
+ else {
90
+ if (!rectIntersects(r, outer))
91
+ hide.push(r.id);
92
+ }
93
+ }
94
+ return { show, hide };
95
+ }
96
+ //# sourceMappingURL=virtualize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"virtualize.js","sourceRoot":"","sources":["../src/virtualize.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,150 @@
1
+ import { Container } from 'pixi.js';
2
+ import { type Node, type WidgetSpec, type WidgetStyle } from '@xenolithengine/graph-core';
3
+ import type { XenTokens } from '@xenolithengine/graph-theme-xen';
4
+ /** Context handed to a custom widget's draw/pointer callbacks. Coords are widget-local CSS px.
5
+ * Theme colours (`accent`/`text`/`muted`) come from the resolved widget tokens so a canvas widget
6
+ * can match the active theme (gold on Xen, cyan on Liquid Glass) instead of hardcoding — unless
7
+ * the widget's own `style` overrode them. */
8
+ export interface CustomWidgetContext {
9
+ value: unknown;
10
+ node: Node;
11
+ width: number;
12
+ height: number;
13
+ accent: string;
14
+ text: string;
15
+ muted: string;
16
+ }
17
+ /** Canvas-draw custom widget — paints into a 2D canvas we own (uploaded to a texture; the primary,
18
+ * perf-friendly path, no foreignObject). `onPointer` returns the new value (or undefined for no
19
+ * change) during a drag; the editor commits it. */
20
+ export interface CanvasWidgetController {
21
+ draw(ctx: CanvasRenderingContext2D, c: CustomWidgetContext): void;
22
+ onPointer?(phase: 'down' | 'move' | 'up', x: number, y: number, c: CustomWidgetContext): unknown;
23
+ }
24
+ /** DOM-mounted custom widget — the editor positions a real HTML element over the widget (in screen
25
+ * space, kept in sync with pan/zoom/drag). This is the contract the React/Vue/Svelte adapters wrap
26
+ * (mount a framework component into `el`); vanilla implements it directly. `setValue` commits
27
+ * (undoable). `update` fires on external value changes. Return a cleanup fn from `mount` or
28
+ * implement `unmount`. */
29
+ export interface DomWidgetController {
30
+ mount(el: HTMLElement, c: CustomWidgetContext & {
31
+ setValue: (v: unknown) => void;
32
+ }): void | (() => void);
33
+ update?(c: CustomWidgetContext): void;
34
+ unmount?(): void;
35
+ }
36
+ /** A custom widget controller — either canvas-draw (fast, WebGL texture) or DOM-mounted (arbitrary
37
+ * HTML / framework component). `editor.registerWidget(name, controller)` accepts either. */
38
+ export type CustomWidgetController = CanvasWidgetController | DomWidgetController;
39
+ export declare function isDomWidgetController(c: CustomWidgetController): c is DomWidgetController;
40
+ /** Fully-resolved widget visuals: the theme's widget tokens with a widget's own `style` override
41
+ * applied on top. Both the WebGL renderer and the editor's DOM editor resolve through this so a
42
+ * per-widget override customises every surface consistently. */
43
+ export interface ResolvedWidgetStyle {
44
+ bg: string;
45
+ bgHover: string;
46
+ bgFocused: string;
47
+ track: string;
48
+ fill: string;
49
+ fillAlpha: number;
50
+ text: string;
51
+ label: string;
52
+ placeholder: string;
53
+ border: string;
54
+ borderFocused: string;
55
+ selection: string;
56
+ knob: string;
57
+ radius: number;
58
+ paddingX: number;
59
+ paddingY: number;
60
+ borderWidth: number;
61
+ toggleWidth: number;
62
+ toggleHeight: number;
63
+ }
64
+ export declare function resolveWidgetStyle(tokens: XenTokens, override?: WidgetStyle): ResolvedWidgetStyle;
65
+ /** The active widget theme as CSS custom properties. The editor sets these on each DOM-mounted
66
+ * custom widget's host element, so a widget author (any framework, or vanilla) can style with
67
+ * `var(--xeno-accent)`, `var(--xeno-bg)`, … and look correct in every theme — and restyle for free
68
+ * when the theme changes. This is the basis for a theme-agnostic widget/plugin ecosystem. */
69
+ export declare function widgetCssVars(c: ResolvedWidgetStyle): Record<string, string>;
70
+ /** The active theme as panel/control CSS custom properties. The editor sets these on the host root
71
+ * so in-editor chrome — `<XenolithPanel>`, `<XenolithControls>`, buttons, any framework component
72
+ * portalled into `editor.overlayRoot` — styles with `var(--xeno-panel)`, `var(--xeno-accent)`, …
73
+ * and restyles for free on `setTheme`. Complements `widgetCssVars` (per-widget input surfaces);
74
+ * shares the `--xeno-accent`/`--xeno-text`/`--xeno-muted`/`--xeno-border`/`--xeno-radius` vocabulary. */
75
+ export declare function themeCssVars(tokens: XenTokens): Record<string, string>;
76
+ export interface WidgetLayoutTokens {
77
+ node: {
78
+ headerHeight: number;
79
+ };
80
+ /** `diameter` + `labelGap` make widget rects share the same content columns as the pin labels
81
+ * above them (left edge at the leftmost pin's label x, right edge at the rightmost) — `paddingX`
82
+ * is used as a fallback only when pin geometry is absent (legacy callers). */
83
+ pin: {
84
+ rowSpacing: number;
85
+ rowHeight: number;
86
+ diameter?: number;
87
+ labelGap?: number;
88
+ };
89
+ header: {
90
+ toPinsGap: number;
91
+ };
92
+ widget: {
93
+ rowHeight: number;
94
+ gap: number;
95
+ paddingX: number;
96
+ };
97
+ }
98
+ export interface WidgetRect {
99
+ id: string;
100
+ x: number;
101
+ y: number;
102
+ width: number;
103
+ height: number;
104
+ }
105
+ /** Context used to resolve pin-bound widget visibility against the live graph: which pins are
106
+ * connected (drives `'whenDisconnected'` hiding) and what value the runtime currently produces
107
+ * on a connected pin (drives `'always'` widgets that display live data). */
108
+ export interface ComputeWidgetRectsCtx {
109
+ /** True when the IN-pin identified by `pinKey` (matched against pin label, then id) has ≥1
110
+ * incoming edge. */
111
+ isPinConnected: (pinKey: string) => boolean;
112
+ /** When a host runtime is loaded, returns the value currently flowing into the bound pin
113
+ * (i.e. the upstream node's last produced output). A `'always'` (display) widget reads this
114
+ * in preference to its stored state when the pin is connected. Undefined when no runtime is
115
+ * attached or the value isn't available. */
116
+ pinLiveValue?: (pinKey: string) => unknown;
117
+ }
118
+ /** Pick the value a widget should display this frame. The contract: a widget bound to an IN-pin
119
+ * with `visibility:'always'` is a DISPLAY widget — when the pin is wired it shows the upstream
120
+ * runtime value (via the host's `pinLiveValue` callback); otherwise it shows its stored state.
121
+ * Applies to BOTH custom and built-in widgets (slider/number/text/combo/color/toggle) — the
122
+ * promise is the same regardless of widget type. */
123
+ export declare function resolveDisplayValue(spec: WidgetSpec, node: Node, ctx?: {
124
+ isPinConnected: (k: string) => boolean;
125
+ pinLiveValue?: (k: string) => unknown;
126
+ }): unknown;
127
+ /** Local-space rects for every visible widget. The canon: every widget is bound to one IN-pin
128
+ * (via `widgetBindKey`) and renders INSIDE that pin's row — pin dot + label on the left, control
129
+ * on the right. `button` widgets aren't pin-bound — they stack in an "actions row" under the pin
130
+ * block. Visibility per widget follows `widgetVisibility`: input controls hide on connect, custom
131
+ * widgets display live runtime values when connected. Returns rects in source order; pin-bound
132
+ * ones whose pin can't be resolved (or is hoisted to the header) are silently skipped. */
133
+ export declare function computeWidgetRects(node: Node, width: number, tokens: WidgetLayoutTokens, ctx?: ComputeWidgetRectsCtx): WidgetRect[];
134
+ export interface WidgetHit {
135
+ id: string;
136
+ spec: WidgetSpec;
137
+ rect: WidgetRect;
138
+ }
139
+ export interface WidgetsView {
140
+ container: Container;
141
+ rects: WidgetRect[];
142
+ /** Hit-test a node-local point; returns the widget under it (skips disabled / button-less). */
143
+ widgetHit(localX: number, localY: number): WidgetHit | null;
144
+ /** Cheaply refresh one widget's visual to a new value (used during live slider/number drag). */
145
+ update(id: string, value: unknown): void;
146
+ }
147
+ /** Builds the WebGL widget rows for a node's expanded form. Pure PIXI — interaction is wired by the
148
+ * editor against `widgetHit`. */
149
+ export declare function renderWidgets(node: Node, width: number, tokens: XenTokens, layoutTokens: WidgetLayoutTokens, customWidgets?: ReadonlyMap<string, CustomWidgetController>, ctx?: ComputeWidgetRectsCtx): WidgetsView;
150
+ //# sourceMappingURL=widget-renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"widget-renderer.d.ts","sourceRoot":"","sources":["../src/widget-renderer.ts"],"names":[],"mappings":"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"}