@invana/graph-layer-bubble-sets 0.0.1

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.
@@ -0,0 +1,253 @@
1
+ import { EventMap, WorldLayer, WorldLayerHit, LayerOptions, CanvasContext } from '@invana/canvas';
2
+
3
+ /**
4
+ * Public type surface for `@invana/graph-layer-bubble-sets`.
5
+ *
6
+ * The layer paints one **set** per group of node ids the user declares. Each
7
+ * set produces a single smooth contour that encloses the member nodes (and
8
+ * their selected edges, if provided) while routing around every other node
9
+ * in the source `GraphLayer`. See `bubblesets-js` for the underlying
10
+ * algorithm — these options surface its compute knobs plus per-set
11
+ * presentation.
12
+ */
13
+
14
+ /** Per-set visual style. Resolved against {@link BUBBLE_SET_STYLE_DEFAULTS}. */
15
+ interface BubbleSetStyle {
16
+ /** Solid fill colour `0xRRGGBB`. Default `0x9c88ff`. */
17
+ fill?: number;
18
+ /** Fill alpha 0..1. Default `0.25`. */
19
+ fillOpacity?: number;
20
+ /** Stroke colour `0xRRGGBB`. Default same as {@link fill}. */
21
+ stroke?: number;
22
+ /** Stroke alpha 0..1. Default `0.9`. */
23
+ strokeOpacity?: number;
24
+ /** Stroke width in world units. Default `1.5`. */
25
+ strokeWidth?: number;
26
+ }
27
+ /**
28
+ * Optional label printed on the set's contour. Styling pulls from the set's
29
+ * own {@link BubbleSetStyle} (background = `fill` at full opacity, text
30
+ * picked for contrast). The flat field is intentionally minimal; richer
31
+ * label control lands once we settle on a layer-wide label primitive.
32
+ */
33
+ interface BubbleSetLabel {
34
+ /** Required label text. */
35
+ text: string;
36
+ /**
37
+ * Where to anchor the label.
38
+ * - `'contour-end'` (default) — the last point of the contour, rotated to
39
+ * match the local tangent. Matches G6's BubbleSets label placement.
40
+ * - `'centroid'` — average of contour points, no rotation.
41
+ */
42
+ placement?: 'contour-end' | 'centroid';
43
+ /** Override text colour. Default contrasts with the set's fill. */
44
+ color?: number;
45
+ /** Font size in world units. Default `11`. */
46
+ fontSize?: number;
47
+ }
48
+ /** A named, declarative grouping of nodes (and optionally edges). */
49
+ interface BubbleSet {
50
+ /** Stable identity. Used as the set key for {@link updateSet}/{@link removeSet}. */
51
+ id: string;
52
+ /** Ids of {@link GraphNode}s to enclose. Required; an empty array skips paint. */
53
+ members: readonly string[];
54
+ /**
55
+ * Optional ids of {@link GraphEdge}s to enclose. The layer feeds each
56
+ * edge as a straight `source-center → target-center` segment to
57
+ * BubbleSets' router; the algorithm morphs the contour to wrap them.
58
+ * Edges whose endpoints aren't both members are still accepted —
59
+ * useful for "include the bridging edge in this set's blob".
60
+ */
61
+ edges?: readonly string[];
62
+ /** Per-set visual style; merged into {@link BUBBLE_SET_STYLE_DEFAULTS}. */
63
+ style?: BubbleSetStyle;
64
+ /** Optional label drawn over the contour. */
65
+ label?: BubbleSetLabel;
66
+ }
67
+ /**
68
+ * Options for {@link BubbleSetsLayer}. The shape mirrors
69
+ * `@invana/graph-layer-d3-contour`: cross-layer dep + algorithm knobs +
70
+ * `recompute` lifecycle, all optional except `graphLayerId` and `sets`.
71
+ */
72
+ interface BubbleSetsLayerOptions {
73
+ /**
74
+ * Required. Id of the `GraphLayer` whose nodes feed the algorithm. Per
75
+ * canvas architecture: cross-layer deps are declared explicitly, never
76
+ * inferred. Throws on mount if the id can't be resolved.
77
+ */
78
+ graphLayerId: string;
79
+ /** Initial set list. May be mutated post-mount via {@link BubbleSetsLayer.setSets}. */
80
+ sets: readonly BubbleSet[];
81
+ /**
82
+ * Grid resolution in square world units. Smaller = sharper contours but
83
+ * quadratically more compute. `bubblesets-js` default: `4`.
84
+ */
85
+ pixelGroup?: number;
86
+ /**
87
+ * Node-influence inner / outer radii (world units). Members attract the
88
+ * contour out to {@link nodeR0} (full influence) and fall off to
89
+ * {@link nodeR1} (zero influence); non-members repel over the same
90
+ * envelope. Defaults: `15` / `50`.
91
+ */
92
+ nodeR0?: number;
93
+ nodeR1?: number;
94
+ /**
95
+ * Edge-influence inner / outer radii (world units). Defaults: `10` / `20`.
96
+ */
97
+ edgeR0?: number;
98
+ edgeR1?: number;
99
+ /**
100
+ * Padding added around the energy grid before sampling — keeps the
101
+ * contour from clipping against the grid border. World units. Default `10`.
102
+ */
103
+ morphBuffer?: number;
104
+ /**
105
+ * Max routing iterations the algorithm runs to find a path that wraps
106
+ * obstacles. Default `100`.
107
+ */
108
+ maxRoutingIterations?: number;
109
+ /**
110
+ * Max marching-squares refinement iterations. Default `20`.
111
+ */
112
+ maxMarchingIterations?: number;
113
+ /**
114
+ * Contour smoothing.
115
+ * - `'chaikin'` (default) — Chaikin's corner-cutting subdivision applied
116
+ * to a sparsified copy of the marching-squares polyline. Produces the
117
+ * roundest, most organic curves; the iteration count is tunable via
118
+ * {@link chaikinIterations}.
119
+ * - `'bspline'` — `PointPath.sample().bSplines()`, the canonical
120
+ * `bubblesets-js` / G6 pipeline. Slightly tighter to the member nodes
121
+ * than Chaikin, less "puffy".
122
+ * - `'none'` — raw marching-squares polyline (jagged).
123
+ */
124
+ smoothness?: 'none' | 'bspline' | 'chaikin';
125
+ /**
126
+ * Number of Chaikin corner-cutting iterations when {@link smoothness} is
127
+ * `'chaikin'`. Each iteration doubles the point count and rounds every
128
+ * corner further; `4` is enough for visually smooth curves on graph-sized
129
+ * inputs. Ignored otherwise. Default `4`.
130
+ */
131
+ chaikinIterations?: number;
132
+ /**
133
+ * Recompute trigger:
134
+ * - `'auto'` (default) — subscribe to the source layer's `data:changed`
135
+ * and recompute on a debounce.
136
+ * - `'manual'` — caller drives recompute via `layer.recompute()`.
137
+ */
138
+ recompute?: 'auto' | 'manual';
139
+ /** Debounce window for `auto` recomputes. Default `120` ms. */
140
+ recomputeDebounceMs?: number;
141
+ }
142
+ /**
143
+ * Reserved. Set geometry is held as a private field, not in `Layer.state`,
144
+ * because it's bulk geometry that's rebuilt wholesale on each recompute
145
+ * rather than diffed.
146
+ */
147
+ interface BubbleSetsLayerState {
148
+ readonly _placeholder?: never;
149
+ }
150
+ interface BubbleSetsLayerEvents extends EventMap {
151
+ /** Fired after each full recompute, before paint. */
152
+ recompute: {
153
+ sets: number;
154
+ durationMs: number;
155
+ };
156
+ /** Fired once per set after it's painted. */
157
+ 'set:painted': {
158
+ setId: string;
159
+ vertices: number;
160
+ };
161
+ }
162
+ /** Style defaults applied per set when fields are absent. */
163
+ declare const BUBBLE_SET_STYLE_DEFAULTS: {
164
+ readonly fill: 10258687;
165
+ readonly fillOpacity: 0.25;
166
+ readonly strokeOpacity: 0.9;
167
+ readonly strokeWidth: 1.5;
168
+ };
169
+ /** Algorithm-side defaults. Mirrors `bubblesets-js` defaults where possible. */
170
+ declare const BUBBLE_SETS_LAYER_DEFAULTS: {
171
+ readonly pixelGroup: 4;
172
+ readonly nodeR0: 15;
173
+ readonly nodeR1: 50;
174
+ readonly edgeR0: 10;
175
+ readonly edgeR1: 20;
176
+ readonly morphBuffer: 10;
177
+ readonly maxRoutingIterations: 100;
178
+ readonly maxMarchingIterations: 20;
179
+ readonly smoothness: "none" | "bspline" | "chaikin";
180
+ readonly chaikinIterations: 4;
181
+ readonly recompute: "auto" | "manual";
182
+ readonly recomputeDebounceMs: 120;
183
+ };
184
+
185
+ /**
186
+ * `BubbleSetsLayer` — `WorldLayer` that paints one smooth contour per
187
+ * declared set of node ids over a source `GraphLayer`. Backed by
188
+ * [`bubblesets-js`](https://github.com/upsetjs/bubblesets-js) (Collins's
189
+ * IEEE InfoVis 2009 algorithm).
190
+ *
191
+ * The compute lives in world space so contours track the graph under
192
+ * camera pan and zoom. Recompute is debounced (default 120 ms) and
193
+ * triggered by the source `GraphLayer`'s `data:changed`. BubbleSets is
194
+ * O(members · grid²); per-frame recompute during a live drag would tank
195
+ * perf — set `recompute: 'manual'` and call `layer.recompute()` from a
196
+ * drag behaviour for that case.
197
+ *
198
+ * Set membership is data the layer owns. Mutate it via {@link setSets} /
199
+ * {@link addSet} / {@link removeSet} / {@link updateSet}; each mutation
200
+ * schedules the same debounced recompute as `data:changed`.
201
+ */
202
+
203
+ declare class BubbleSetsLayer extends WorldLayer<BubbleSetsLayerOptions, BubbleSetsLayerState, BubbleSetsLayerEvents, never, WorldLayerHit> {
204
+ private readonly graphLayerId;
205
+ private sets;
206
+ private graph;
207
+ private gfx;
208
+ private labels;
209
+ private readonly subs;
210
+ private debounceTimer;
211
+ constructor(opts: LayerOptions<BubbleSetsLayerOptions>);
212
+ protected createState(): BubbleSetsLayerState;
213
+ protected onMount(ctx: CanvasContext): void;
214
+ protected onUnmount(): void;
215
+ hitTest(_worldX: number, _worldY: number): WorldLayerHit | null;
216
+ /** Replace the full set list. */
217
+ setSets(sets: readonly BubbleSet[]): void;
218
+ /** Append a set. No-op (with warning) if the id already exists. */
219
+ addSet(set: BubbleSet): void;
220
+ /** Remove a set by id. Returns `true` if anything was removed. */
221
+ removeSet(id: string): boolean;
222
+ /**
223
+ * Shallow-merge `patch` into the set with the given id. Nested `style` /
224
+ * `label` are also shallow-merged so callers can supply partial style /
225
+ * label patches without rebuilding the whole object. Returns `true` if
226
+ * the id was found.
227
+ */
228
+ updateSet(id: string, patch: Partial<Omit<BubbleSet, 'id'>>): boolean;
229
+ /** Read-only view of the current set list. */
230
+ getSets(): readonly BubbleSet[];
231
+ /**
232
+ * Force an immediate recompute. Useful in `recompute: 'manual'` mode, or
233
+ * to refresh the overlay after externally mutating options that don't
234
+ * have setters yet.
235
+ */
236
+ recompute(): void;
237
+ private scheduleRecompute;
238
+ private computeAndPaint;
239
+ /**
240
+ * World-space AABB for a node. `GraphLayer.boundsOfNode` returns the
241
+ * shape's local (centre-relative) rect — `node.position` is *not* baked
242
+ * in — so we offset by the node's position to get world coords. Falls
243
+ * back to a small box around the position when the renderer hasn't
244
+ * mounted the node yet.
245
+ */
246
+ private rectForNode;
247
+ private algorithmOptions;
248
+ private paintSet;
249
+ private paintLabel;
250
+ private emitRecompute;
251
+ }
252
+
253
+ export { BUBBLE_SETS_LAYER_DEFAULTS, BUBBLE_SET_STYLE_DEFAULTS, type BubbleSet, type BubbleSetLabel, type BubbleSetStyle, BubbleSetsLayer, type BubbleSetsLayerEvents, type BubbleSetsLayerOptions, type BubbleSetsLayerState };
package/dist/index.js ADDED
@@ -0,0 +1,344 @@
1
+ import { WorldLayer } from '@invana/canvas';
2
+ import { createOutline, PointPath } from 'bubblesets-js';
3
+ import { Text, Graphics } from 'pixi.js';
4
+
5
+ // src/BubbleSetsLayer.ts
6
+
7
+ // src/types.ts
8
+ var BUBBLE_SET_STYLE_DEFAULTS = {
9
+ fill: 10258687,
10
+ fillOpacity: 0.25,
11
+ strokeOpacity: 0.9,
12
+ strokeWidth: 1.5
13
+ };
14
+ var BUBBLE_SETS_LAYER_DEFAULTS = {
15
+ pixelGroup: 4,
16
+ nodeR0: 15,
17
+ nodeR1: 50,
18
+ edgeR0: 10,
19
+ edgeR1: 20,
20
+ morphBuffer: 10,
21
+ maxRoutingIterations: 100,
22
+ maxMarchingIterations: 20,
23
+ smoothness: "chaikin",
24
+ chaikinIterations: 4,
25
+ recompute: "auto",
26
+ recomputeDebounceMs: 120
27
+ };
28
+
29
+ // src/BubbleSetsLayer.ts
30
+ var BubbleSetsLayer = class extends WorldLayer {
31
+ graphLayerId;
32
+ sets;
33
+ graph = null;
34
+ gfx = null;
35
+ labels = null;
36
+ subs = [];
37
+ // Browser `setTimeout` returns `number`; using `ReturnType<typeof setTimeout>`
38
+ // would resolve to NodeJS.Timeout in dual-typed environments and break the
39
+ // `window.clearTimeout(...)` call site.
40
+ debounceTimer = null;
41
+ constructor(opts) {
42
+ super({
43
+ ...opts,
44
+ // Contours extend past node centres by node-influence + morph buffer,
45
+ // so viewport culling against the bare node AABB would clip them.
46
+ cullable: opts.cullable ?? false,
47
+ // Passive annotation — clicks fall through to the graph below.
48
+ hittable: opts.hittable ?? false
49
+ });
50
+ this.graphLayerId = opts.options.graphLayerId;
51
+ this.sets = [...opts.options.sets];
52
+ }
53
+ createState() {
54
+ return {};
55
+ }
56
+ onMount(ctx) {
57
+ const graph = ctx.layers.get(this.graphLayerId);
58
+ if (!graph) {
59
+ throw new Error(
60
+ `BubbleSetsLayer "${this.id}": graph layer "${this.graphLayerId}" not found. Add the GraphLayer before this annotation layer.`
61
+ );
62
+ }
63
+ this.graph = graph;
64
+ this.gfx = this.createGraphics("bubble-sets-contours");
65
+ this.labels = this.createContainer("bubble-sets-labels");
66
+ const recompute = this.options.recompute ?? BUBBLE_SETS_LAYER_DEFAULTS.recompute;
67
+ if (recompute === "auto") {
68
+ this.subs.push(graph.events.on("data:changed", () => this.scheduleRecompute()));
69
+ }
70
+ this.scheduleRecompute();
71
+ }
72
+ onUnmount() {
73
+ for (const off of this.subs) off();
74
+ this.subs.length = 0;
75
+ if (this.debounceTimer !== null) {
76
+ window.clearTimeout(this.debounceTimer);
77
+ this.debounceTimer = null;
78
+ }
79
+ this.gfx = null;
80
+ this.labels = null;
81
+ this.graph = null;
82
+ }
83
+ hitTest(_worldX, _worldY) {
84
+ return null;
85
+ }
86
+ // ─── Set mutators ─────────────────────────────────────────────────────────
87
+ // Each mutation owns recompute-scheduling so callers never have to remember
88
+ // to call `recompute()` themselves. Mirrors the resolver-based live-tweaking
89
+ // pattern documented in [[feedback_live_edge_tweaking_via_resolvers]].
90
+ /** Replace the full set list. */
91
+ setSets(sets) {
92
+ this.sets = [...sets];
93
+ this.scheduleRecompute();
94
+ }
95
+ /** Append a set. No-op (with warning) if the id already exists. */
96
+ addSet(set) {
97
+ if (this.sets.some((s) => s.id === set.id)) {
98
+ console.warn(`BubbleSetsLayer "${this.id}": addSet \u2014 id "${set.id}" already present.`);
99
+ return;
100
+ }
101
+ this.sets.push(set);
102
+ this.scheduleRecompute();
103
+ }
104
+ /** Remove a set by id. Returns `true` if anything was removed. */
105
+ removeSet(id) {
106
+ const i = this.sets.findIndex((s) => s.id === id);
107
+ if (i === -1) return false;
108
+ this.sets.splice(i, 1);
109
+ this.scheduleRecompute();
110
+ return true;
111
+ }
112
+ /**
113
+ * Shallow-merge `patch` into the set with the given id. Nested `style` /
114
+ * `label` are also shallow-merged so callers can supply partial style /
115
+ * label patches without rebuilding the whole object. Returns `true` if
116
+ * the id was found.
117
+ */
118
+ updateSet(id, patch) {
119
+ const i = this.sets.findIndex((s) => s.id === id);
120
+ if (i === -1) return false;
121
+ const prev = this.sets[i];
122
+ this.sets[i] = {
123
+ ...prev,
124
+ ...patch,
125
+ style: patch.style ? { ...prev.style, ...patch.style } : prev.style,
126
+ label: patch.label ? { ...prev.label, ...patch.label } : prev.label
127
+ };
128
+ this.scheduleRecompute();
129
+ return true;
130
+ }
131
+ /** Read-only view of the current set list. */
132
+ getSets() {
133
+ return this.sets;
134
+ }
135
+ /**
136
+ * Force an immediate recompute. Useful in `recompute: 'manual'` mode, or
137
+ * to refresh the overlay after externally mutating options that don't
138
+ * have setters yet.
139
+ */
140
+ recompute() {
141
+ this.computeAndPaint();
142
+ }
143
+ // ─── Internals ────────────────────────────────────────────────────────────
144
+ scheduleRecompute() {
145
+ if (!this.gfx) return;
146
+ if (this.debounceTimer !== null) window.clearTimeout(this.debounceTimer);
147
+ const wait = this.options.recomputeDebounceMs ?? BUBBLE_SETS_LAYER_DEFAULTS.recomputeDebounceMs;
148
+ this.debounceTimer = window.setTimeout(() => {
149
+ this.debounceTimer = null;
150
+ this.computeAndPaint();
151
+ }, wait);
152
+ }
153
+ computeAndPaint() {
154
+ const g = this.gfx;
155
+ const labels = this.labels;
156
+ const graph = this.graph;
157
+ if (!g || !labels || !graph) return;
158
+ const t0 = performance.now();
159
+ g.clear();
160
+ labels.removeChildren().forEach((c) => c.destroy());
161
+ if (this.sets.length === 0) {
162
+ this.emitRecompute(0, t0);
163
+ return;
164
+ }
165
+ const nodeRects = /* @__PURE__ */ new Map();
166
+ for (const node of graph.store.nodes()) {
167
+ const r = this.rectForNode(graph, node);
168
+ if (r) nodeRects.set(node.id, r);
169
+ }
170
+ const algoOpts = this.algorithmOptions();
171
+ const smoothness = this.options.smoothness ?? BUBBLE_SETS_LAYER_DEFAULTS.smoothness;
172
+ for (const set of this.sets) {
173
+ if (set.members.length === 0) continue;
174
+ const members = [];
175
+ const nonMemberIds = new Set(nodeRects.keys());
176
+ for (const id of set.members) {
177
+ const r = nodeRects.get(id);
178
+ if (!r) continue;
179
+ members.push(r);
180
+ nonMemberIds.delete(id);
181
+ }
182
+ if (members.length === 0) continue;
183
+ const nonMembers = [];
184
+ for (const id of nonMemberIds) nonMembers.push(nodeRects.get(id));
185
+ const edges = [];
186
+ if (set.edges) {
187
+ for (const eid of set.edges) {
188
+ const edge = graph.store.getEdge(eid);
189
+ if (!edge) continue;
190
+ const s = graph.store.getNode(edge.source)?.position;
191
+ const t = graph.store.getNode(edge.target)?.position;
192
+ if (!s || !t) continue;
193
+ edges.push({ x1: s.x, y1: s.y, x2: t.x, y2: t.y });
194
+ }
195
+ }
196
+ let path = createOutline(members, nonMembers, edges, algoOpts);
197
+ if (smoothness === "bspline") {
198
+ path = path.sample().bSplines();
199
+ } else if (smoothness === "chaikin") {
200
+ path = chaikin(
201
+ path.sample(),
202
+ this.options.chaikinIterations ?? BUBBLE_SETS_LAYER_DEFAULTS.chaikinIterations
203
+ );
204
+ }
205
+ this.paintSet(g, labels, set, path);
206
+ }
207
+ this.emitRecompute(this.sets.length, t0);
208
+ }
209
+ /**
210
+ * World-space AABB for a node. `GraphLayer.boundsOfNode` returns the
211
+ * shape's local (centre-relative) rect — `node.position` is *not* baked
212
+ * in — so we offset by the node's position to get world coords. Falls
213
+ * back to a small box around the position when the renderer hasn't
214
+ * mounted the node yet.
215
+ */
216
+ rectForNode(graph, node) {
217
+ const p = node.position;
218
+ if (!p) return null;
219
+ const b = graph.boundsOfNode(node);
220
+ if (b) return { x: b.x + p.x, y: b.y + p.y, width: b.width, height: b.height };
221
+ const r = 10;
222
+ return { x: p.x - r, y: p.y - r, width: r * 2, height: r * 2 };
223
+ }
224
+ algorithmOptions() {
225
+ const o = this.options;
226
+ return {
227
+ pixelGroup: o.pixelGroup ?? BUBBLE_SETS_LAYER_DEFAULTS.pixelGroup,
228
+ nodeR0: o.nodeR0 ?? BUBBLE_SETS_LAYER_DEFAULTS.nodeR0,
229
+ nodeR1: o.nodeR1 ?? BUBBLE_SETS_LAYER_DEFAULTS.nodeR1,
230
+ edgeR0: o.edgeR0 ?? BUBBLE_SETS_LAYER_DEFAULTS.edgeR0,
231
+ edgeR1: o.edgeR1 ?? BUBBLE_SETS_LAYER_DEFAULTS.edgeR1,
232
+ morphBuffer: o.morphBuffer ?? BUBBLE_SETS_LAYER_DEFAULTS.morphBuffer,
233
+ maxRoutingIterations: o.maxRoutingIterations ?? BUBBLE_SETS_LAYER_DEFAULTS.maxRoutingIterations,
234
+ maxMarchingIterations: o.maxMarchingIterations ?? BUBBLE_SETS_LAYER_DEFAULTS.maxMarchingIterations
235
+ };
236
+ }
237
+ paintSet(g, labels, set, path) {
238
+ const pts = path.points;
239
+ if (pts.length < 3) return;
240
+ const style = { ...BUBBLE_SET_STYLE_DEFAULTS, ...set.style };
241
+ const fill = style.fill;
242
+ const stroke = set.style?.stroke ?? fill;
243
+ tracedSmoothClosedPath(g, pts);
244
+ g.fill({ color: fill, alpha: style.fillOpacity });
245
+ tracedSmoothClosedPath(g, pts);
246
+ g.stroke({
247
+ color: stroke,
248
+ alpha: style.strokeOpacity,
249
+ width: style.strokeWidth,
250
+ join: "round",
251
+ cap: "round"
252
+ });
253
+ if (set.label) this.paintLabel(labels, set, pts, stroke);
254
+ this.events.emit("set:painted", { setId: set.id, vertices: pts.length });
255
+ }
256
+ paintLabel(labels, set, pts, fallbackColor) {
257
+ const label = set.label;
258
+ const placement = label.placement ?? "contour-end";
259
+ let anchorX;
260
+ let anchorY;
261
+ let rotation = 0;
262
+ if (placement === "centroid") {
263
+ let sx = 0;
264
+ let sy = 0;
265
+ for (const p of pts) {
266
+ sx += p.x;
267
+ sy += p.y;
268
+ }
269
+ anchorX = sx / pts.length;
270
+ anchorY = sy / pts.length;
271
+ } else {
272
+ const end = pts[pts.length - 1];
273
+ const prev = pts[Math.max(0, pts.length - 8)];
274
+ anchorX = end.x;
275
+ anchorY = end.y;
276
+ rotation = Math.atan2(end.y - prev.y, end.x - prev.x);
277
+ }
278
+ const fontSize = label.fontSize ?? 11;
279
+ const text = new Text({
280
+ text: label.text,
281
+ style: {
282
+ fontFamily: "system-ui, -apple-system, sans-serif",
283
+ fontSize,
284
+ fontWeight: "600",
285
+ fill: label.color ?? 16777215,
286
+ padding: 2
287
+ }
288
+ });
289
+ text.anchor.set(0.5);
290
+ text.x = anchorX;
291
+ text.y = anchorY;
292
+ text.rotation = rotation;
293
+ const bg = new Graphics();
294
+ const w = text.width + 12;
295
+ const h = text.height + 4;
296
+ bg.roundRect(-w / 2, -h / 2, w, h, Math.min(h / 2, 6));
297
+ bg.fill({ color: set.style?.stroke ?? fallbackColor, alpha: 0.95 });
298
+ bg.x = anchorX;
299
+ bg.y = anchorY;
300
+ bg.rotation = rotation;
301
+ labels.addChild(bg);
302
+ labels.addChild(text);
303
+ }
304
+ emitRecompute(sets, t0) {
305
+ this.events.emit("recompute", { sets, durationMs: performance.now() - t0 });
306
+ }
307
+ };
308
+ function tracedSmoothClosedPath(g, pts) {
309
+ const n = pts.length;
310
+ if (n < 3) return;
311
+ const last = pts[n - 1];
312
+ const first = pts[0];
313
+ let mx = (last.x + first.x) * 0.5;
314
+ let my = (last.y + first.y) * 0.5;
315
+ g.moveTo(mx, my);
316
+ for (let i = 0; i < n; i++) {
317
+ const a = pts[i];
318
+ const b = pts[(i + 1) % n];
319
+ mx = (a.x + b.x) * 0.5;
320
+ my = (a.y + b.y) * 0.5;
321
+ g.quadraticCurveTo(a.x, a.y, mx, my);
322
+ }
323
+ g.closePath();
324
+ }
325
+ function chaikin(path, iterations) {
326
+ let pts = path.points;
327
+ for (let it = 0; it < iterations; it++) {
328
+ const n = pts.length;
329
+ if (n < 3) break;
330
+ const next = new Array(n * 2);
331
+ for (let i = 0; i < n; i++) {
332
+ const a = pts[i];
333
+ const b = pts[(i + 1) % n];
334
+ next[i * 2] = { x: 0.75 * a.x + 0.25 * b.x, y: 0.75 * a.y + 0.25 * b.y };
335
+ next[i * 2 + 1] = { x: 0.25 * a.x + 0.75 * b.x, y: 0.25 * a.y + 0.75 * b.y };
336
+ }
337
+ pts = next;
338
+ }
339
+ return new PointPath(pts, path.closed);
340
+ }
341
+
342
+ export { BUBBLE_SETS_LAYER_DEFAULTS, BUBBLE_SET_STYLE_DEFAULTS, BubbleSetsLayer };
343
+ //# sourceMappingURL=index.js.map
344
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/BubbleSetsLayer.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Public type surface for `@invana/graph-layer-bubble-sets`.\n *\n * The layer paints one **set** per group of node ids the user declares. Each\n * set produces a single smooth contour that encloses the member nodes (and\n * their selected edges, if provided) while routing around every other node\n * in the source `GraphLayer`. See `bubblesets-js` for the underlying\n * algorithm — these options surface its compute knobs plus per-set\n * presentation.\n */\nimport type { EventMap } from '@invana/canvas';\n\n/** Per-set visual style. Resolved against {@link BUBBLE_SET_STYLE_DEFAULTS}. */\nexport interface BubbleSetStyle {\n /** Solid fill colour `0xRRGGBB`. Default `0x9c88ff`. */\n fill?: number;\n /** Fill alpha 0..1. Default `0.25`. */\n fillOpacity?: number;\n /** Stroke colour `0xRRGGBB`. Default same as {@link fill}. */\n stroke?: number;\n /** Stroke alpha 0..1. Default `0.9`. */\n strokeOpacity?: number;\n /** Stroke width in world units. Default `1.5`. */\n strokeWidth?: number;\n}\n\n/**\n * Optional label printed on the set's contour. Styling pulls from the set's\n * own {@link BubbleSetStyle} (background = `fill` at full opacity, text\n * picked for contrast). The flat field is intentionally minimal; richer\n * label control lands once we settle on a layer-wide label primitive.\n */\nexport interface BubbleSetLabel {\n /** Required label text. */\n text: string;\n /**\n * Where to anchor the label.\n * - `'contour-end'` (default) — the last point of the contour, rotated to\n * match the local tangent. Matches G6's BubbleSets label placement.\n * - `'centroid'` — average of contour points, no rotation.\n */\n placement?: 'contour-end' | 'centroid';\n /** Override text colour. Default contrasts with the set's fill. */\n color?: number;\n /** Font size in world units. Default `11`. */\n fontSize?: number;\n}\n\n/** A named, declarative grouping of nodes (and optionally edges). */\nexport interface BubbleSet {\n /** Stable identity. Used as the set key for {@link updateSet}/{@link removeSet}. */\n id: string;\n /** Ids of {@link GraphNode}s to enclose. Required; an empty array skips paint. */\n members: readonly string[];\n /**\n * Optional ids of {@link GraphEdge}s to enclose. The layer feeds each\n * edge as a straight `source-center → target-center` segment to\n * BubbleSets' router; the algorithm morphs the contour to wrap them.\n * Edges whose endpoints aren't both members are still accepted —\n * useful for \"include the bridging edge in this set's blob\".\n */\n edges?: readonly string[];\n /** Per-set visual style; merged into {@link BUBBLE_SET_STYLE_DEFAULTS}. */\n style?: BubbleSetStyle;\n /** Optional label drawn over the contour. */\n label?: BubbleSetLabel;\n}\n\n/**\n * Options for {@link BubbleSetsLayer}. The shape mirrors\n * `@invana/graph-layer-d3-contour`: cross-layer dep + algorithm knobs +\n * `recompute` lifecycle, all optional except `graphLayerId` and `sets`.\n */\nexport interface BubbleSetsLayerOptions {\n /**\n * Required. Id of the `GraphLayer` whose nodes feed the algorithm. Per\n * canvas architecture: cross-layer deps are declared explicitly, never\n * inferred. Throws on mount if the id can't be resolved.\n */\n graphLayerId: string;\n\n /** Initial set list. May be mutated post-mount via {@link BubbleSetsLayer.setSets}. */\n sets: readonly BubbleSet[];\n\n /**\n * Grid resolution in square world units. Smaller = sharper contours but\n * quadratically more compute. `bubblesets-js` default: `4`.\n */\n pixelGroup?: number;\n\n /**\n * Node-influence inner / outer radii (world units). Members attract the\n * contour out to {@link nodeR0} (full influence) and fall off to\n * {@link nodeR1} (zero influence); non-members repel over the same\n * envelope. Defaults: `15` / `50`.\n */\n nodeR0?: number;\n nodeR1?: number;\n\n /**\n * Edge-influence inner / outer radii (world units). Defaults: `10` / `20`.\n */\n edgeR0?: number;\n edgeR1?: number;\n\n /**\n * Padding added around the energy grid before sampling — keeps the\n * contour from clipping against the grid border. World units. Default `10`.\n */\n morphBuffer?: number;\n\n /**\n * Max routing iterations the algorithm runs to find a path that wraps\n * obstacles. Default `100`.\n */\n maxRoutingIterations?: number;\n\n /**\n * Max marching-squares refinement iterations. Default `20`.\n */\n maxMarchingIterations?: number;\n\n /**\n * Contour smoothing.\n * - `'chaikin'` (default) — Chaikin's corner-cutting subdivision applied\n * to a sparsified copy of the marching-squares polyline. Produces the\n * roundest, most organic curves; the iteration count is tunable via\n * {@link chaikinIterations}.\n * - `'bspline'` — `PointPath.sample().bSplines()`, the canonical\n * `bubblesets-js` / G6 pipeline. Slightly tighter to the member nodes\n * than Chaikin, less \"puffy\".\n * - `'none'` — raw marching-squares polyline (jagged).\n */\n smoothness?: 'none' | 'bspline' | 'chaikin';\n\n /**\n * Number of Chaikin corner-cutting iterations when {@link smoothness} is\n * `'chaikin'`. Each iteration doubles the point count and rounds every\n * corner further; `4` is enough for visually smooth curves on graph-sized\n * inputs. Ignored otherwise. Default `4`.\n */\n chaikinIterations?: number;\n\n /**\n * Recompute trigger:\n * - `'auto'` (default) — subscribe to the source layer's `data:changed`\n * and recompute on a debounce.\n * - `'manual'` — caller drives recompute via `layer.recompute()`.\n */\n recompute?: 'auto' | 'manual';\n\n /** Debounce window for `auto` recomputes. Default `120` ms. */\n recomputeDebounceMs?: number;\n}\n\n/**\n * Reserved. Set geometry is held as a private field, not in `Layer.state`,\n * because it's bulk geometry that's rebuilt wholesale on each recompute\n * rather than diffed.\n */\nexport interface BubbleSetsLayerState {\n readonly _placeholder?: never;\n}\n\nexport interface BubbleSetsLayerEvents extends EventMap {\n /** Fired after each full recompute, before paint. */\n recompute: { sets: number; durationMs: number };\n /** Fired once per set after it's painted. */\n 'set:painted': { setId: string; vertices: number };\n}\n\n/** Style defaults applied per set when fields are absent. */\nexport const BUBBLE_SET_STYLE_DEFAULTS = {\n fill: 0x9c88ff,\n fillOpacity: 0.25,\n strokeOpacity: 0.9,\n strokeWidth: 1.5,\n} as const;\n\n/** Algorithm-side defaults. Mirrors `bubblesets-js` defaults where possible. */\nexport const BUBBLE_SETS_LAYER_DEFAULTS = {\n pixelGroup: 4,\n nodeR0: 15,\n nodeR1: 50,\n edgeR0: 10,\n edgeR1: 20,\n morphBuffer: 10,\n maxRoutingIterations: 100,\n maxMarchingIterations: 20,\n smoothness: 'chaikin' as 'none' | 'bspline' | 'chaikin',\n chaikinIterations: 4,\n recompute: 'auto' as 'auto' | 'manual',\n recomputeDebounceMs: 120,\n} as const;\n","/**\n * `BubbleSetsLayer` — `WorldLayer` that paints one smooth contour per\n * declared set of node ids over a source `GraphLayer`. Backed by\n * [`bubblesets-js`](https://github.com/upsetjs/bubblesets-js) (Collins's\n * IEEE InfoVis 2009 algorithm).\n *\n * The compute lives in world space so contours track the graph under\n * camera pan and zoom. Recompute is debounced (default 120 ms) and\n * triggered by the source `GraphLayer`'s `data:changed`. BubbleSets is\n * O(members · grid²); per-frame recompute during a live drag would tank\n * perf — set `recompute: 'manual'` and call `layer.recompute()` from a\n * drag behaviour for that case.\n *\n * Set membership is data the layer owns. Mutate it via {@link setSets} /\n * {@link addSet} / {@link removeSet} / {@link updateSet}; each mutation\n * schedules the same debounced recompute as `data:changed`.\n */\n\nimport { WorldLayer } from '@invana/canvas';\nimport type { CanvasContext, LayerOptions, WorldLayerHit } from '@invana/canvas';\nimport { GraphLayer } from '@invana/graph';\nimport type { GraphNode } from '@invana/graph';\nimport {\n createOutline,\n PointPath,\n type IRectangle,\n type ILine,\n} from 'bubblesets-js';\nimport { Container, Graphics, Text } from 'pixi.js';\n\nimport {\n BUBBLE_SET_STYLE_DEFAULTS,\n BUBBLE_SETS_LAYER_DEFAULTS,\n type BubbleSet,\n type BubbleSetsLayerEvents,\n type BubbleSetsLayerOptions,\n type BubbleSetsLayerState,\n} from './types';\n\nexport class BubbleSetsLayer extends WorldLayer<\n BubbleSetsLayerOptions,\n BubbleSetsLayerState,\n BubbleSetsLayerEvents,\n never,\n WorldLayerHit\n> {\n private readonly graphLayerId: string;\n private sets: BubbleSet[];\n\n private graph: GraphLayer | null = null;\n private gfx: Graphics | null = null;\n private labels: Container | null = null;\n private readonly subs: Array<() => void> = [];\n\n // Browser `setTimeout` returns `number`; using `ReturnType<typeof setTimeout>`\n // would resolve to NodeJS.Timeout in dual-typed environments and break the\n // `window.clearTimeout(...)` call site.\n private debounceTimer: number | null = null;\n\n constructor(opts: LayerOptions<BubbleSetsLayerOptions>) {\n super({\n ...opts,\n // Contours extend past node centres by node-influence + morph buffer,\n // so viewport culling against the bare node AABB would clip them.\n cullable: opts.cullable ?? false,\n // Passive annotation — clicks fall through to the graph below.\n hittable: opts.hittable ?? false,\n });\n this.graphLayerId = opts.options.graphLayerId;\n // Defensive copy — caller may mutate the array they passed in.\n this.sets = [...opts.options.sets];\n }\n\n protected createState(): BubbleSetsLayerState {\n return {};\n }\n\n protected override onMount(ctx: CanvasContext): void {\n const graph = ctx.layers.get<GraphLayer>(this.graphLayerId);\n if (!graph) {\n throw new Error(\n `BubbleSetsLayer \"${this.id}\": graph layer \"${this.graphLayerId}\" not found. ` +\n `Add the GraphLayer before this annotation layer.`,\n );\n }\n this.graph = graph;\n this.gfx = this.createGraphics('bubble-sets-contours');\n this.labels = this.createContainer('bubble-sets-labels');\n\n const recompute = this.options.recompute ?? BUBBLE_SETS_LAYER_DEFAULTS.recompute;\n if (recompute === 'auto') {\n this.subs.push(graph.events.on('data:changed', () => this.scheduleRecompute()));\n }\n\n // Initial paint — the graph may already hold data when we mount.\n this.scheduleRecompute();\n }\n\n protected override onUnmount(): void {\n for (const off of this.subs) off();\n this.subs.length = 0;\n if (this.debounceTimer !== null) {\n window.clearTimeout(this.debounceTimer);\n this.debounceTimer = null;\n }\n this.gfx = null;\n this.labels = null;\n this.graph = null;\n }\n\n hitTest(_worldX: number, _worldY: number): WorldLayerHit | null {\n return null;\n }\n\n // ─── Set mutators ─────────────────────────────────────────────────────────\n // Each mutation owns recompute-scheduling so callers never have to remember\n // to call `recompute()` themselves. Mirrors the resolver-based live-tweaking\n // pattern documented in [[feedback_live_edge_tweaking_via_resolvers]].\n\n /** Replace the full set list. */\n setSets(sets: readonly BubbleSet[]): void {\n this.sets = [...sets];\n this.scheduleRecompute();\n }\n\n /** Append a set. No-op (with warning) if the id already exists. */\n addSet(set: BubbleSet): void {\n if (this.sets.some((s) => s.id === set.id)) {\n console.warn(`BubbleSetsLayer \"${this.id}\": addSet — id \"${set.id}\" already present.`);\n return;\n }\n this.sets.push(set);\n this.scheduleRecompute();\n }\n\n /** Remove a set by id. Returns `true` if anything was removed. */\n removeSet(id: string): boolean {\n const i = this.sets.findIndex((s) => s.id === id);\n if (i === -1) return false;\n this.sets.splice(i, 1);\n this.scheduleRecompute();\n return true;\n }\n\n /**\n * Shallow-merge `patch` into the set with the given id. Nested `style` /\n * `label` are also shallow-merged so callers can supply partial style /\n * label patches without rebuilding the whole object. Returns `true` if\n * the id was found.\n */\n updateSet(id: string, patch: Partial<Omit<BubbleSet, 'id'>>): boolean {\n const i = this.sets.findIndex((s) => s.id === id);\n if (i === -1) return false;\n const prev = this.sets[i]!;\n this.sets[i] = {\n ...prev,\n ...patch,\n style: patch.style ? { ...prev.style, ...patch.style } : prev.style,\n label: patch.label ? { ...prev.label, ...patch.label } : prev.label,\n };\n this.scheduleRecompute();\n return true;\n }\n\n /** Read-only view of the current set list. */\n getSets(): readonly BubbleSet[] {\n return this.sets;\n }\n\n /**\n * Force an immediate recompute. Useful in `recompute: 'manual'` mode, or\n * to refresh the overlay after externally mutating options that don't\n * have setters yet.\n */\n recompute(): void {\n this.computeAndPaint();\n }\n\n // ─── Internals ────────────────────────────────────────────────────────────\n\n private scheduleRecompute(): void {\n // Pre-mount mutations are absorbed by the initial paint scheduled in\n // `onMount`; nothing to do until `gfx` exists.\n if (!this.gfx) return;\n if (this.debounceTimer !== null) window.clearTimeout(this.debounceTimer);\n const wait =\n this.options.recomputeDebounceMs ?? BUBBLE_SETS_LAYER_DEFAULTS.recomputeDebounceMs;\n this.debounceTimer = window.setTimeout(() => {\n this.debounceTimer = null;\n this.computeAndPaint();\n }, wait);\n }\n\n private computeAndPaint(): void {\n const g = this.gfx;\n const labels = this.labels;\n const graph = this.graph;\n if (!g || !labels || !graph) return;\n\n const t0 = performance.now();\n\n g.clear();\n labels.removeChildren().forEach((c) => c.destroy());\n\n if (this.sets.length === 0) {\n this.emitRecompute(0, t0);\n return;\n }\n\n // Bounds for every node in the source graph, keyed by id. Computed\n // once per recompute, then partitioned into member / non-member per\n // set. `boundsOfNode` returns `undefined` for nodes the renderer\n // hasn't mounted yet (rare during initial sync) — skip them.\n const nodeRects = new Map<string, IRectangle>();\n for (const node of graph.store.nodes()) {\n const r = this.rectForNode(graph, node);\n if (r) nodeRects.set(node.id, r);\n }\n\n const algoOpts = this.algorithmOptions();\n const smoothness = this.options.smoothness ?? BUBBLE_SETS_LAYER_DEFAULTS.smoothness;\n\n for (const set of this.sets) {\n if (set.members.length === 0) continue;\n\n const members: IRectangle[] = [];\n const nonMemberIds = new Set(nodeRects.keys());\n for (const id of set.members) {\n const r = nodeRects.get(id);\n if (!r) continue;\n members.push(r);\n nonMemberIds.delete(id);\n }\n if (members.length === 0) continue;\n\n const nonMembers: IRectangle[] = [];\n for (const id of nonMemberIds) nonMembers.push(nodeRects.get(id)!);\n\n const edges: ILine[] = [];\n if (set.edges) {\n for (const eid of set.edges) {\n const edge = graph.store.getEdge(eid);\n if (!edge) continue;\n const s = graph.store.getNode(edge.source)?.position;\n const t = graph.store.getNode(edge.target)?.position;\n if (!s || !t) continue;\n edges.push({ x1: s.x, y1: s.y, x2: t.x, y2: t.y });\n }\n }\n\n let path: PointPath = createOutline(members, nonMembers, edges, algoOpts);\n if (smoothness === 'bspline') {\n // Canonical bubblesets-js / G6 pipeline: sparsify first so the\n // b-spline has room to round, then interpolate.\n path = path.sample().bSplines();\n } else if (smoothness === 'chaikin') {\n path = chaikin(\n path.sample(),\n this.options.chaikinIterations ?? BUBBLE_SETS_LAYER_DEFAULTS.chaikinIterations,\n );\n }\n\n this.paintSet(g, labels, set, path);\n }\n\n this.emitRecompute(this.sets.length, t0);\n }\n\n /**\n * World-space AABB for a node. `GraphLayer.boundsOfNode` returns the\n * shape's local (centre-relative) rect — `node.position` is *not* baked\n * in — so we offset by the node's position to get world coords. Falls\n * back to a small box around the position when the renderer hasn't\n * mounted the node yet.\n */\n private rectForNode(graph: GraphLayer, node: GraphNode): IRectangle | null {\n const p = node.position;\n if (!p) return null;\n const b = graph.boundsOfNode(node);\n if (b) return { x: b.x + p.x, y: b.y + p.y, width: b.width, height: b.height };\n const r = 10;\n return { x: p.x - r, y: p.y - r, width: r * 2, height: r * 2 };\n }\n\n private algorithmOptions() {\n const o = this.options;\n return {\n pixelGroup: o.pixelGroup ?? BUBBLE_SETS_LAYER_DEFAULTS.pixelGroup,\n nodeR0: o.nodeR0 ?? BUBBLE_SETS_LAYER_DEFAULTS.nodeR0,\n nodeR1: o.nodeR1 ?? BUBBLE_SETS_LAYER_DEFAULTS.nodeR1,\n edgeR0: o.edgeR0 ?? BUBBLE_SETS_LAYER_DEFAULTS.edgeR0,\n edgeR1: o.edgeR1 ?? BUBBLE_SETS_LAYER_DEFAULTS.edgeR1,\n morphBuffer: o.morphBuffer ?? BUBBLE_SETS_LAYER_DEFAULTS.morphBuffer,\n maxRoutingIterations:\n o.maxRoutingIterations ?? BUBBLE_SETS_LAYER_DEFAULTS.maxRoutingIterations,\n maxMarchingIterations:\n o.maxMarchingIterations ?? BUBBLE_SETS_LAYER_DEFAULTS.maxMarchingIterations,\n };\n }\n\n private paintSet(g: Graphics, labels: Container, set: BubbleSet, path: PointPath): void {\n const pts = path.points;\n if (pts.length < 3) return;\n\n const style = { ...BUBBLE_SET_STYLE_DEFAULTS, ...set.style };\n const fill = style.fill;\n const stroke = set.style?.stroke ?? fill;\n\n // Build the contour as a closed quadratic-Bezier spline through\n // segment midpoints, not a straight polyline. Each input point is used\n // as an off-curve quadratic control point, so the rendered curve is\n // C¹ continuous regardless of the input's stair-stepping. This is what\n // turns marching-squares output into a glassy contour at draw time —\n // independent of how many smoothing iterations ran upstream.\n tracedSmoothClosedPath(g, pts);\n g.fill({ color: fill, alpha: style.fillOpacity });\n tracedSmoothClosedPath(g, pts);\n g.stroke({\n color: stroke,\n alpha: style.strokeOpacity,\n width: style.strokeWidth,\n join: 'round',\n cap: 'round',\n });\n\n if (set.label) this.paintLabel(labels, set, pts, stroke);\n\n this.events.emit('set:painted', { setId: set.id, vertices: pts.length });\n }\n\n private paintLabel(\n labels: Container,\n set: BubbleSet,\n pts: ReadonlyArray<{ x: number; y: number }>,\n fallbackColor: number,\n ): void {\n const label = set.label!;\n const placement = label.placement ?? 'contour-end';\n\n let anchorX: number;\n let anchorY: number;\n let rotation = 0;\n\n if (placement === 'centroid') {\n let sx = 0;\n let sy = 0;\n for (const p of pts) {\n sx += p.x;\n sy += p.y;\n }\n anchorX = sx / pts.length;\n anchorY = sy / pts.length;\n } else {\n // `contour-end` — last point, rotated along the local tangent.\n const end = pts[pts.length - 1]!;\n const prev = pts[Math.max(0, pts.length - 8)]!;\n anchorX = end.x;\n anchorY = end.y;\n rotation = Math.atan2(end.y - prev.y, end.x - prev.x);\n }\n\n const fontSize = label.fontSize ?? 11;\n const text = new Text({\n text: label.text,\n style: {\n fontFamily: 'system-ui, -apple-system, sans-serif',\n fontSize,\n fontWeight: '600',\n fill: label.color ?? 0xffffff,\n padding: 2,\n },\n });\n text.anchor.set(0.5);\n text.x = anchorX;\n text.y = anchorY;\n text.rotation = rotation;\n\n // Background pill behind the text, sized to the rendered Text.\n const bg = new Graphics();\n const w = text.width + 12;\n const h = text.height + 4;\n bg.roundRect(-w / 2, -h / 2, w, h, Math.min(h / 2, 6));\n bg.fill({ color: set.style?.stroke ?? fallbackColor, alpha: 0.95 });\n bg.x = anchorX;\n bg.y = anchorY;\n bg.rotation = rotation;\n\n labels.addChild(bg);\n labels.addChild(text);\n }\n\n private emitRecompute(sets: number, t0: number): void {\n this.events.emit('recompute', { sets, durationMs: performance.now() - t0 });\n }\n}\n\n/**\n * Trace a closed polyline as a quadratic-Bezier spline through segment\n * midpoints. The standard polyline-midpoint trick: start at the midpoint\n * of the last→first segment, then for each input point `p_i` issue\n * `quadraticCurveTo(p_i, midpoint(p_i, p_{i+1}))`. Each input point acts\n * as an off-curve control; the rendered curve passes through every\n * midpoint and is C¹ continuous.\n *\n * Effect: any polyline — even one with marching-squares stair-steps —\n * draws as a smooth curve. The smoothing happens at the draw layer, so\n * we no longer pay for it in the smoothing pipeline.\n */\nfunction tracedSmoothClosedPath(g: Graphics, pts: ReadonlyArray<{ x: number; y: number }>): void {\n const n = pts.length;\n if (n < 3) return;\n const last = pts[n - 1]!;\n const first = pts[0]!;\n let mx = (last.x + first.x) * 0.5;\n let my = (last.y + first.y) * 0.5;\n g.moveTo(mx, my);\n for (let i = 0; i < n; i++) {\n const a = pts[i]!;\n const b = pts[(i + 1) % n]!;\n mx = (a.x + b.x) * 0.5;\n my = (a.y + b.y) * 0.5;\n g.quadraticCurveTo(a.x, a.y, mx, my);\n }\n g.closePath();\n}\n\n/**\n * Chaikin's corner-cutting subdivision. Each iteration replaces every\n * polyline corner with two new points at 1/4 and 3/4 of the way along\n * the adjacent edges, doubling the point count and rounding every corner.\n *\n * BubbleSets contours are always closed, so the algorithm wraps around\n * index `n-1 → 0` to keep the seam smooth.\n */\nfunction chaikin(path: PointPath, iterations: number): PointPath {\n let pts: ReadonlyArray<{ x: number; y: number }> = path.points;\n for (let it = 0; it < iterations; it++) {\n const n = pts.length;\n if (n < 3) break;\n const next: Array<{ x: number; y: number }> = new Array(n * 2);\n for (let i = 0; i < n; i++) {\n const a = pts[i]!;\n const b = pts[(i + 1) % n]!;\n next[i * 2] = { x: 0.75 * a.x + 0.25 * b.x, y: 0.75 * a.y + 0.25 * b.y };\n next[i * 2 + 1] = { x: 0.25 * a.x + 0.75 * b.x, y: 0.25 * a.y + 0.75 * b.y };\n }\n pts = next;\n }\n return new PointPath(pts, path.closed);\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@invana/graph-layer-bubble-sets",
3
+ "version": "0.0.1",
4
+ "description": "BubbleSets annotation layer for @invana/graph — smooth contours around named node groups, with edge enclosure and non-member routing.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "typedocOptions": {
9
+ "entryPoints": [
10
+ "src/index.ts"
11
+ ]
12
+ },
13
+ "dependencies": {
14
+ "bubblesets-js": "^3.0.1"
15
+ },
16
+ "peerDependencies": {
17
+ "pixi.js": "^8.18.1",
18
+ "@invana/canvas": "0.0.1",
19
+ "@invana/graph": "0.0.1"
20
+ },
21
+ "devDependencies": {
22
+ "pixi.js": "^8.18.1",
23
+ "tsup": "^8.3.5",
24
+ "typescript": "5.9.2",
25
+ "@invana/canvas": "0.0.1",
26
+ "@invana/graph": "0.0.1",
27
+ "@repo/typescript-config": "0.0.0"
28
+ },
29
+ "keywords": [
30
+ "canvas",
31
+ "graph",
32
+ "layer",
33
+ "annotation",
34
+ "bubblesets",
35
+ "bubble-sets",
36
+ "set-cover",
37
+ "contour"
38
+ ],
39
+ "license": "Apache-2.0",
40
+ "module": "./dist/index.js",
41
+ "exports": {
42
+ ".": {
43
+ "types": "./dist/index.d.ts",
44
+ "import": "./dist/index.js",
45
+ "default": "./dist/index.js"
46
+ }
47
+ },
48
+ "files": [
49
+ "dist"
50
+ ],
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "scripts": {
55
+ "build": "tsup",
56
+ "dev": "tsup --watch",
57
+ "lint": "eslint src/",
58
+ "check-types": "tsc --noEmit",
59
+ "clean": "rm -rf dist"
60
+ }
61
+ }