@kortexya/nodus 0.1.0 → 0.1.2
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/nodus.src.bundle.js +4413 -3587
- package/package.json +6 -3
- package/types/hypergraph/render/BubbleSets.d.ts +45 -0
- package/types/hypergraph/render/Interaction.d.ts +4 -0
- package/types/hypergraph/render/PolygonRenderer.d.ts +5 -0
- package/types/hypergraph/types.d.ts +5 -0
- package/types/internals/Hypergraph.d.ts +33 -0
- package/types/modules/HypergraphAPI.d.ts +4 -0
- package/types/renderers/WasmGraphRenderer.d.ts +60 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kortexya/nodus",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Nodus
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Nodus — a high-performance graph visualization engine (WebGL/WebGPU/Canvas/SVG).",
|
|
5
5
|
"homepage": "https://kortexya.com",
|
|
6
6
|
"author": "Kortexya <david.loiret@kortexya.com>",
|
|
7
7
|
"license": "UNLICENSED",
|
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
"clean:dist": "rm -f nodus.src.bundle.js nodus_wasm-*.js nodus_render_wasm-*.js __vite-plugin-wasm-helper-*.js chunk-*.js && rm -rf assets types",
|
|
20
20
|
"build:types": "tsc -p tsconfig.json --declaration --emitDeclarationOnly --noEmit false --outDir types",
|
|
21
21
|
"prepublishOnly": "npm run build",
|
|
22
|
-
"release": "bash scripts/release.sh"
|
|
22
|
+
"release": "bash scripts/release.sh",
|
|
23
|
+
"docs:dev": "npm --prefix docs run dev",
|
|
24
|
+
"docs:build": "npm --prefix docs run build",
|
|
25
|
+
"docs:preview": "npm --prefix docs run preview"
|
|
23
26
|
},
|
|
24
27
|
"exports": {
|
|
25
28
|
".": {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bubble Sets — member-tight, non-member-avoiding set boundaries.
|
|
3
|
+
*
|
|
4
|
+
* The naive way to draw a hyperedge is the convex hull of its member vertices
|
|
5
|
+
* (see `monotoneChain`). A convex hull encloses everything between its corners,
|
|
6
|
+
* so it routinely swallows vertices that are NOT members — drawing an overlap
|
|
7
|
+
* that doesn't exist. For a tool whose job is conveying which evidence is shared,
|
|
8
|
+
* that is a correctness bug, not a cosmetic one.
|
|
9
|
+
*
|
|
10
|
+
* Bubble Sets (Collins, Penn & Carpendale, "Bubble Sets: Revealing Set Relations
|
|
11
|
+
* with Isocontours over Existing Visualizations", IEEE TVCG 2009) fixes it with a
|
|
12
|
+
* scalar energy field: member vertices (and the virtual edges that connect them)
|
|
13
|
+
* deposit POSITIVE energy that attracts the contour; non-member vertices deposit
|
|
14
|
+
* NEGATIVE energy that repels it. The boundary is the isocontour at a threshold,
|
|
15
|
+
* traced with marching squares and smoothed. The result hugs the members, bridges
|
|
16
|
+
* them into one organic blob, and carves AROUND (or punches a hole through) any
|
|
17
|
+
* non-member caught in the middle.
|
|
18
|
+
*
|
|
19
|
+
* Everything here is in world coordinates and camera-independent, so the result
|
|
20
|
+
* is cached by the caller and only recomputed when vertex positions change.
|
|
21
|
+
*/
|
|
22
|
+
import type { Vec2 } from '../types';
|
|
23
|
+
export interface BubbleSetOptions {
|
|
24
|
+
/** Characteristic length L (world units). Derived from member spacing when omitted. */
|
|
25
|
+
scale?: number;
|
|
26
|
+
memberR0Factor?: number;
|
|
27
|
+
memberR1Factor?: number;
|
|
28
|
+
edgeR1Factor?: number;
|
|
29
|
+
nmR0Factor?: number;
|
|
30
|
+
nmR1Factor?: number;
|
|
31
|
+
threshold?: number;
|
|
32
|
+
memberInfluence?: number;
|
|
33
|
+
edgeInfluence?: number;
|
|
34
|
+
nonMemberInfluence?: number;
|
|
35
|
+
smooth?: number;
|
|
36
|
+
maxGrid?: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Compute the boundary of a set as one or more closed world-space loops.
|
|
40
|
+
* Multiple loops happen when the set is genuinely disconnected, or when a
|
|
41
|
+
* non-member punches a hole — render them with the even-odd fill rule.
|
|
42
|
+
*/
|
|
43
|
+
export declare function computeBubbleSet(members: Vec2[], nonMembers: Vec2[], opts?: BubbleSetOptions): Vec2[][];
|
|
44
|
+
/** Even-odd point-in-set test against a set of loops (matches the fill rule). */
|
|
45
|
+
export declare function pointInLoops(x: number, y: number, loops: Vec2[][]): boolean;
|
|
@@ -15,6 +15,10 @@ export interface HitTestState {
|
|
|
15
15
|
vertexRadius: number;
|
|
16
16
|
/** 'primal' | 'dual' | 'both' — which set of polygons to hit-test. */
|
|
17
17
|
view: 'primal' | 'dual' | 'both';
|
|
18
|
+
/** Bubble-set boundary loops per hyperedge — when present, the cursor is
|
|
19
|
+
* tested against the SAME shape that's drawn (so hover matches the polygon
|
|
20
|
+
* and never fires for a non-member region). Falls back to the convex hull. */
|
|
21
|
+
contours?: Map<HypergraphId, Vec2[][]>;
|
|
18
22
|
}
|
|
19
23
|
export interface HitResult {
|
|
20
24
|
hyperedge: HypergraphId | null;
|
|
@@ -18,6 +18,11 @@ export interface PolygonRenderState {
|
|
|
18
18
|
options: Required<Omit<HypergraphRenderOptions, 'palette'>> & {
|
|
19
19
|
palette: HypergraphRenderOptions['palette'];
|
|
20
20
|
};
|
|
21
|
+
/** Precomputed world-space boundary loops per hyperedge (Bubble Sets). When
|
|
22
|
+
* present (flat primal view), polygons are drawn/filled from these instead
|
|
23
|
+
* of the convex hull — so the boundary excludes non-members. Multiple loops
|
|
24
|
+
* per hyperedge are filled even-odd (holes around interior non-members). */
|
|
25
|
+
contours?: Map<HypergraphId, Vec2[][]>;
|
|
21
26
|
/** Currently-hovered primal hyperedge id. */
|
|
22
27
|
hoveredHyperedge?: HypergraphId | null;
|
|
23
28
|
/** Hovered dual hyperedge (corresponds to a primal vertex). */
|
|
@@ -145,6 +145,11 @@ export interface HypergraphRenderOptions {
|
|
|
145
145
|
* draws a small label badge at the vertex position. Used for
|
|
146
146
|
* Aït-Kaci coreference X-tags. */
|
|
147
147
|
vertexTag?: (v: HypergraphVertex) => string | null | undefined;
|
|
148
|
+
/** Hyperedge boundary style for the flat primal view. `'bubble'` (default)
|
|
149
|
+
* draws member-tight Bubble Sets that exclude non-member vertices; `'hull'`
|
|
150
|
+
* draws the legacy convex hull (faster, but encloses non-members). 2.5D and
|
|
151
|
+
* dual views always use the convex hull. */
|
|
152
|
+
boundary?: 'bubble' | 'hull';
|
|
148
153
|
}
|
|
149
154
|
export interface InteractionEvent {
|
|
150
155
|
/** Hyperedge id under cursor, if any. */
|
|
@@ -36,6 +36,10 @@ export declare class Hypergraph extends Module {
|
|
|
36
36
|
/** Last applied render options. */
|
|
37
37
|
renderOptions: PolygonRenderState['options'] | null;
|
|
38
38
|
private _layers;
|
|
39
|
+
/** Cached Bubble-Set boundary loops, keyed by a positions+structure
|
|
40
|
+
* signature so they recompute only when vertices actually move (not on
|
|
41
|
+
* every camera pan/zoom frame). */
|
|
42
|
+
private _contourCache;
|
|
39
43
|
hoveredHyperedge: HypergraphId | null;
|
|
40
44
|
hoveredDualOf: HypergraphId | null;
|
|
41
45
|
hoveredVertex: HypergraphId | null;
|
|
@@ -58,6 +62,15 @@ export declare class Hypergraph extends Module {
|
|
|
58
62
|
* 3. undefined — caller skips this vertex.
|
|
59
63
|
*/
|
|
60
64
|
private _resolvePosition;
|
|
65
|
+
/**
|
|
66
|
+
* Push apart any vertices the optimizer left (near-)coincident. When two
|
|
67
|
+
* vertices that belong to DIFFERENT hyperedges land on top of each other, no
|
|
68
|
+
* region boundary can contain one while excluding the other — so the polygon
|
|
69
|
+
* is forced to swallow a non-member. A light relaxation that only moves pairs
|
|
70
|
+
* closer than the minimum spacing (and leaves everything else untouched)
|
|
71
|
+
* removes that pathology while preserving the optimized layout.
|
|
72
|
+
*/
|
|
73
|
+
private _separateVertices;
|
|
61
74
|
/** Build a live positions map for the renderer — combines optimizer
|
|
62
75
|
* output with bound Nodus node positions. */
|
|
63
76
|
private _livePositions;
|
|
@@ -85,6 +98,22 @@ export declare class Hypergraph extends Module {
|
|
|
85
98
|
getDualPositions(): Map<HypergraphId, Vec2>;
|
|
86
99
|
/** CCW convex-hull polygon for one hyperedge — uses live positions. */
|
|
87
100
|
getHyperedgePolygon(heId: HypergraphId): Vec2[];
|
|
101
|
+
/** Member-tight Bubble-Set boundary loops for one hyperedge (the SHAPE the
|
|
102
|
+
* renderer actually draws in the flat primal view). Multiple loops = a
|
|
103
|
+
* disconnected set or a hole carved around an interior non-member; test
|
|
104
|
+
* containment even-odd. Falls back to the convex hull when bubble boundaries
|
|
105
|
+
* are disabled or unavailable. */
|
|
106
|
+
getHyperedgeContours(heId: HypergraphId): Vec2[][];
|
|
107
|
+
/**
|
|
108
|
+
* Compute (and cache) the Bubble-Set boundary loops for every hyperedge.
|
|
109
|
+
* Members of a hyperedge attract the contour; all OTHER vertices repel it,
|
|
110
|
+
* so the boundary hugs the members and routes around / holes out anything
|
|
111
|
+
* that isn't one of them — unlike the convex hull, which swallows them.
|
|
112
|
+
*
|
|
113
|
+
* Cached by a cheap positions+structure signature so a static graph computes
|
|
114
|
+
* this once and every subsequent camera frame reuses it.
|
|
115
|
+
*/
|
|
116
|
+
private _ensureContours;
|
|
88
117
|
render(opts?: HypergraphRenderOptions): RenderHandles;
|
|
89
118
|
private _bindInteraction;
|
|
90
119
|
private _unbindInteraction;
|
|
@@ -101,6 +130,10 @@ export declare class Hypergraph extends Module {
|
|
|
101
130
|
}): void;
|
|
102
131
|
clearSelection(): void;
|
|
103
132
|
getHyperedgeAtPoint(world: Vec2): InteractionEvent;
|
|
133
|
+
/** ALL hyperedges whose drawn boundary contains `world` (≥2 ⇒ the cursor is
|
|
134
|
+
* in a real overlap region). Uses the same Bubble-Set contours as the
|
|
135
|
+
* renderer + hit-test, so it never reports a non-member region as overlap. */
|
|
136
|
+
getHyperedgesAtPoint(world: Vec2): HypergraphId[];
|
|
104
137
|
refreshLayers(): void;
|
|
105
138
|
removeLayers(): void;
|
|
106
139
|
setActiveView(view: 'primal' | 'dual' | 'both'): void;
|
|
@@ -31,6 +31,10 @@ export declare class HypergraphAPI extends APIModule {
|
|
|
31
31
|
getVertexPositions(): Map<HypergraphId, Vec2>;
|
|
32
32
|
getDualPositions(): Map<HypergraphId, Vec2>;
|
|
33
33
|
getHyperedgePolygon(heId: HypergraphId): Vec2[];
|
|
34
|
+
/** Member-tight Bubble-Set boundary loops (the shape actually drawn). */
|
|
35
|
+
getHyperedgeContours(heId: HypergraphId): Vec2[][];
|
|
36
|
+
/** All hyperedges whose drawn boundary contains the world point (overlap). */
|
|
37
|
+
getHyperedgesAtPoint(world: Vec2): HypergraphId[];
|
|
34
38
|
render(opts?: HypergraphRenderOptions): {
|
|
35
39
|
primal?: any;
|
|
36
40
|
dual?: any;
|
|
@@ -25,16 +25,24 @@ export declare class WasmGraphRenderer {
|
|
|
25
25
|
private _raf;
|
|
26
26
|
private _toLinear;
|
|
27
27
|
private _styleVersion;
|
|
28
|
+
private _styleDirty;
|
|
29
|
+
private _frameScheduled;
|
|
28
30
|
private _radii;
|
|
29
31
|
private _fill;
|
|
30
32
|
private _shape;
|
|
31
33
|
private _stroke;
|
|
32
34
|
private _sw;
|
|
35
|
+
private _swMinVis;
|
|
36
|
+
private _layer;
|
|
37
|
+
private _pulses;
|
|
33
38
|
private _outline;
|
|
34
39
|
private _haloCol;
|
|
35
40
|
private _haloW;
|
|
41
|
+
private _outerCol;
|
|
42
|
+
private _outerW;
|
|
36
43
|
private _imageTile;
|
|
37
44
|
private _atlasKey;
|
|
45
|
+
private _pieColors;
|
|
38
46
|
private _pieOffset;
|
|
39
47
|
private _pieCount;
|
|
40
48
|
private _pieSegs;
|
|
@@ -42,14 +50,45 @@ export declare class WasmGraphRenderer {
|
|
|
42
50
|
private _edgeColor;
|
|
43
51
|
private _edgeWidth;
|
|
44
52
|
private _edgeCurv;
|
|
53
|
+
private _edgeArrows;
|
|
54
|
+
private _edgeDash;
|
|
55
|
+
private _edgeHaloCol;
|
|
56
|
+
private _edgeHaloW;
|
|
57
|
+
private _edgeOutlineCol;
|
|
58
|
+
private _edgeOutlineW;
|
|
59
|
+
private _edgeLabel;
|
|
60
|
+
private _edgeLabelFont;
|
|
61
|
+
private _edgeLabelCol;
|
|
62
|
+
private _edgeLabelSize;
|
|
63
|
+
private _edgeLabelSec;
|
|
64
|
+
private _edgeLabelSecFont;
|
|
65
|
+
private _edgeLabelSecCol;
|
|
45
66
|
private _xs;
|
|
46
67
|
private _ys;
|
|
47
68
|
private _nodeIds;
|
|
48
69
|
private _labels;
|
|
49
70
|
private _lastCam;
|
|
50
|
-
private
|
|
71
|
+
private _autoFrame;
|
|
72
|
+
private _selfMovingCamera;
|
|
73
|
+
private _lastFit;
|
|
51
74
|
private _glyphs;
|
|
75
|
+
private _glyphKey;
|
|
76
|
+
private _textFonts;
|
|
77
|
+
private _labelCol;
|
|
78
|
+
private _labelMinVis;
|
|
79
|
+
private _labelMaxLine;
|
|
80
|
+
private _labelSize;
|
|
81
|
+
private _labelSecSize;
|
|
82
|
+
private _labelPos;
|
|
83
|
+
private _labelSecondary;
|
|
84
|
+
private _labelSecCol;
|
|
85
|
+
private _labelSecFont;
|
|
86
|
+
private _labelSecBg;
|
|
87
|
+
private _labelBgCol;
|
|
88
|
+
private _icons;
|
|
52
89
|
private _fontPx;
|
|
90
|
+
private _targetAtlasPx;
|
|
91
|
+
private _quadScale;
|
|
53
92
|
private _highlight;
|
|
54
93
|
private _pieData;
|
|
55
94
|
private _badgeNode;
|
|
@@ -60,13 +99,19 @@ export declare class WasmGraphRenderer {
|
|
|
60
99
|
private _badgeStrokeCol;
|
|
61
100
|
private _badgeStrokeW;
|
|
62
101
|
private _badgeText;
|
|
102
|
+
private _badgeFont;
|
|
63
103
|
private _badgeTextCol;
|
|
64
104
|
private constructor();
|
|
105
|
+
/** Coalesce redraw requests into a single rAF-driven frame (no-op if one is already pending or the
|
|
106
|
+
* environment has no rAF). Keeps event-driven redraws cheap when many events fire in one tick. */
|
|
107
|
+
private _scheduleFrame;
|
|
65
108
|
/** Create a renderer bound to `canvas`. `WasmRendererClass` is the lazy-loaded wgpu class. */
|
|
66
109
|
static create(nodus: any, canvas: HTMLCanvasElement, WasmRendererClass: any, opts?: WasmGraphRendererOptions): Promise<WasmGraphRenderer>;
|
|
67
110
|
/** Rasterise an ASCII glyph atlas (white, alpha=coverage) into a texture the TEXT shader samples,
|
|
68
111
|
* recording each glyph's UV region. Monospace-ish layout in a fixed cell grid. */
|
|
69
112
|
private _buildAtlas;
|
|
113
|
+
/** CELL/ATLAS_PX from `_buildAtlas` — scales a quad so the em renders at the requested font size. */
|
|
114
|
+
private static readonly _QUAD_SCALE;
|
|
70
115
|
/** Load image URLs into a 16×16 tile grid atlas (64px tiles, 1024² sRGB) and upload it; redraw on
|
|
71
116
|
* completion so node icons appear. attrEx.y (set in _buildNodeAttrs) selects each node's tile. */
|
|
72
117
|
private _loadAtlas;
|
|
@@ -76,6 +121,11 @@ export declare class WasmGraphRenderer {
|
|
|
76
121
|
private _buildText;
|
|
77
122
|
private _dpr;
|
|
78
123
|
private _sizeCanvas;
|
|
124
|
+
/** Keep the backing store (and wgpu surface) matched to the canvas's displayed CSS size × DPR.
|
|
125
|
+
* Called every frame — a no-op when the size is unchanged. Without it a responsive container or a
|
|
126
|
+
* canvas sized before layout (clientWidth 0 → 320 fallback at create) renders at a stale, smaller
|
|
127
|
+
* resolution that the browser upscales, which reads as a soft / non-crisp image. */
|
|
128
|
+
private _syncCanvasSize;
|
|
79
129
|
/** getAttribute that returns null instead of throwing on an undefined attribute name. */
|
|
80
130
|
private _safeAttr;
|
|
81
131
|
/** A Nodus style value (false/null = off, colour string, or {color,...}) → LINEAR rgba, or null. */
|
|
@@ -98,8 +148,16 @@ export declare class WasmGraphRenderer {
|
|
|
98
148
|
* [20..24] outerStrokeCol [24..28] outlineCol */
|
|
99
149
|
private _buildNodeAttrs;
|
|
100
150
|
/** Assemble the edge attr SoA the EDGE shader decodes — 2×u32 per edge: packed rgba8 colour, then
|
|
101
|
-
* (
|
|
151
|
+
* (width14 | dash<<14 | headShape<<16 | tailShape<<19 | widthScale<<22 | curvByte<<24). headShape/
|
|
152
|
+
* tailShape are 3-bit extremity shapes (0 none,1 arrow,2 square,3 circle,4 open-arrow). Feature surface. */
|
|
102
153
|
private _buildEdgeAttrs;
|
|
154
|
+
/** Halo edges: a wider, halo-coloured copy of each haloed edge, packed like _buildEdgeAttrs so it can
|
|
155
|
+
* be PREPENDED (drawn behind the real edges). No dash/arrows; width = edgeWidth + 2·haloWidth. */
|
|
156
|
+
private _buildEdgeHalos;
|
|
157
|
+
/** Active pulse rings this frame → instance arrays (a transparent disk with a coloured inner-stroke
|
|
158
|
+
* ring). Each enabled node spawns a ring every `iv` ms; a ring lives `du` ms, growing sr→er·radius
|
|
159
|
+
* while its colour lerps sc→ec (→ transparent). Empty when nothing pulses. */
|
|
160
|
+
private _buildPulseRings;
|
|
103
161
|
/** Draw one frame from the live graph. */
|
|
104
162
|
renderFrame(): void;
|
|
105
163
|
/** Hit-test a canvas-relative screen point (CSS px) → node id, or null. CPU point-in-radius
|