@kortexya/nodus 0.1.1 → 0.1.3

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kortexya/nodus",
3
- "version": "0.1.1",
4
- "description": "Nodus \u2014 a high-performance graph visualization engine (WebGL/WebGPU/Canvas/SVG).",
3
+ "version": "0.1.3",
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",
@@ -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. */
@@ -41,6 +41,15 @@ export declare class Geo extends Module {
41
41
  longitude: number;
42
42
  };
43
43
  }) => Promise<any[]>;
44
+ /**
45
+ * Set geographic coordinates on a list of nodes. `coords[i]` is
46
+ * `{ latitude, longitude }` for `nodes.get(i)`. The values are written to each
47
+ * node's data at the configured latitude/longitude paths, then projected into
48
+ * the internal geo.lat/geo.lng arrays (and re-rendered if geo mode is on).
49
+ * Backs the public `node.setGeoCoordinates()` / `nodeList.setGeoCoordinates()`
50
+ * and the in-map node-drag handler.
51
+ */
52
+ setNodeGeoCoordinates(nodes: any, coords: any[]): Promise<any>;
44
53
  onMounted(): void;
45
54
  private _onDataChange;
46
55
  private _refreshOrAddNodes;
@@ -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;