@simplex-viz/svg 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1121 @@
1
+ import { rectToConvexPolygon, getHullBoundingRadius, SimplexGraphEngine, computeConvexHull, regularPolygon, starConvexHull } from '@simplex-viz/core';
2
+
3
+ // src/hullMeasure.ts
4
+ var SVG_NS = "http://www.w3.org/2000/svg";
5
+ function estimateSvgFallbackBBox(group) {
6
+ let maxHalfW = 0;
7
+ let maxHalfH = 0;
8
+ const children = group.querySelectorAll("rect, circle, ellipse, text");
9
+ children.forEach((el) => {
10
+ const tag = el.tagName.toLowerCase();
11
+ if (tag === "rect") {
12
+ const w = parseFloat(el.getAttribute("width") ?? "0");
13
+ const h = parseFloat(el.getAttribute("height") ?? "0");
14
+ const x = parseFloat(el.getAttribute("x") ?? String(-w / 2));
15
+ const y = parseFloat(el.getAttribute("y") ?? String(-h / 2));
16
+ maxHalfW = Math.max(maxHalfW, Math.abs(x), Math.abs(x + w), w / 2);
17
+ maxHalfH = Math.max(maxHalfH, Math.abs(y), Math.abs(y + h), h / 2);
18
+ } else if (tag === "circle") {
19
+ const r = parseFloat(el.getAttribute("r") ?? "0");
20
+ const cx = parseFloat(el.getAttribute("cx") ?? "0");
21
+ const cy = parseFloat(el.getAttribute("cy") ?? "0");
22
+ maxHalfW = Math.max(maxHalfW, Math.abs(cx) + r);
23
+ maxHalfH = Math.max(maxHalfH, Math.abs(cy) + r);
24
+ } else if (tag === "text") {
25
+ const len = (el.textContent ?? "").length;
26
+ const fs = parseFloat(el.getAttribute("font-size") ?? "11");
27
+ const estW = Math.max(16, len * fs * 0.58);
28
+ const estH = fs * 1.4;
29
+ maxHalfW = Math.max(maxHalfW, estW / 2);
30
+ maxHalfH = Math.max(maxHalfH, estH / 2);
31
+ }
32
+ });
33
+ return {
34
+ width: Math.max(20, maxHalfW * 2),
35
+ height: Math.max(20, maxHalfH * 2)
36
+ };
37
+ }
38
+ function measureVisualWithSvgView(view, visual, cacheKey, rootSvg, measureBin, cache) {
39
+ const cached = cache.get(cacheKey);
40
+ if (cached) return cached;
41
+ let hull;
42
+ if (view.measureConvexHull) {
43
+ hull = view.measureConvexHull(visual, rootSvg);
44
+ } else {
45
+ while (measureBin.firstChild) {
46
+ measureBin.removeChild(measureBin.firstChild);
47
+ }
48
+ const probeGroup = rootSvg.ownerDocument.createElementNS(SVG_NS, "g");
49
+ measureBin.appendChild(probeGroup);
50
+ const dummyHull = { kind: "circle", radius: 16 };
51
+ const dummyState = {
52
+ id: "__measure__",
53
+ x: 0,
54
+ y: 0,
55
+ scale: 1,
56
+ opacity: 1,
57
+ visual,
58
+ hull: dummyHull,
59
+ cacheKey,
60
+ interaction: {
61
+ isHovered: false,
62
+ isPinned: false,
63
+ isDimmed: false,
64
+ isHighlighted: false
65
+ },
66
+ transition: null
67
+ };
68
+ view.mountOrUpdate(probeGroup, visual, dummyState);
69
+ let width = 0;
70
+ let height = 0;
71
+ try {
72
+ if (typeof probeGroup.getBBox === "function") {
73
+ const bbox = probeGroup.getBBox();
74
+ width = bbox.width;
75
+ height = bbox.height;
76
+ }
77
+ } catch {
78
+ }
79
+ if (width <= 1 || height <= 1) {
80
+ const fallback = estimateSvgFallbackBBox(probeGroup);
81
+ width = Math.max(width, fallback.width);
82
+ height = Math.max(height, fallback.height);
83
+ }
84
+ hull = rectToConvexPolygon(width + 2, height + 2, 6);
85
+ while (measureBin.firstChild) {
86
+ measureBin.removeChild(measureBin.firstChild);
87
+ }
88
+ }
89
+ cache.set(cacheKey, hull);
90
+ return hull;
91
+ }
92
+ function interactionSignature(state) {
93
+ const { isHovered, isPinned, isDimmed, isHighlighted } = state.interaction;
94
+ return `${isHovered ? 1 : 0}${isPinned ? 1 : 0}${isDimmed ? 1 : 0}${isHighlighted ? 1 : 0}`;
95
+ }
96
+ var SvgRendererAdapter = class {
97
+ svgEl;
98
+ doc;
99
+ worldGroup;
100
+ trianglesGroup;
101
+ edgesGroup;
102
+ clustersGroup;
103
+ itemsGroup;
104
+ hullsGroup;
105
+ measureBin;
106
+ contentView;
107
+ clusterView;
108
+ contentHullCache = /* @__PURE__ */ new Map();
109
+ clusterHullCache = /* @__PURE__ */ new Map();
110
+ contentViewByKey = /* @__PURE__ */ new Map();
111
+ clusterViewByKey = /* @__PURE__ */ new Map();
112
+ clusterDomMap = /* @__PURE__ */ new Map();
113
+ itemDomMap = /* @__PURE__ */ new Map();
114
+ trianglePolygons = [];
115
+ edgeLines = [];
116
+ constructor(svgEl, options) {
117
+ this.svgEl = svgEl;
118
+ this.doc = svgEl.ownerDocument;
119
+ this.contentView = options.contentView;
120
+ this.clusterView = options.clusterView;
121
+ while (this.svgEl.firstChild) {
122
+ this.svgEl.removeChild(this.svgEl.firstChild);
123
+ }
124
+ this.worldGroup = this.createGroup("sv-world");
125
+ this.trianglesGroup = this.createGroup("sv-mesh-triangles");
126
+ this.edgesGroup = this.createGroup("sv-mesh-edges");
127
+ this.clustersGroup = this.createGroup("sv-clusters");
128
+ this.itemsGroup = this.createGroup("sv-items");
129
+ this.hullsGroup = this.createGroup("sv-hull-overlay");
130
+ this.hullsGroup.setAttribute("pointer-events", "none");
131
+ this.worldGroup.appendChild(this.trianglesGroup);
132
+ this.worldGroup.appendChild(this.edgesGroup);
133
+ this.worldGroup.appendChild(this.clustersGroup);
134
+ this.worldGroup.appendChild(this.itemsGroup);
135
+ this.worldGroup.appendChild(this.hullsGroup);
136
+ this.measureBin = this.createGroup("sv-measure-bin");
137
+ this.measureBin.setAttribute("visibility", "hidden");
138
+ this.measureBin.setAttribute("pointer-events", "none");
139
+ this.measureBin.setAttribute("transform", "translate(-99999, -99999)");
140
+ this.svgEl.appendChild(this.worldGroup);
141
+ this.svgEl.appendChild(this.measureBin);
142
+ }
143
+ createGroup(className) {
144
+ const g = this.doc.createElementNS(SVG_NS, "g");
145
+ g.setAttribute("class", className);
146
+ return g;
147
+ }
148
+ /**
149
+ * Swaps active content and/or cluster `SvgNodeView` plugins at runtime and invalidates
150
+ * hull caches so the engine re-measures and smoothly crossfades into the new views.
151
+ */
152
+ setViews(views) {
153
+ if (views.contentView && views.contentView !== this.contentView) {
154
+ this.contentView = views.contentView;
155
+ this.contentHullCache.clear();
156
+ }
157
+ if (views.clusterView && views.clusterView !== this.clusterView) {
158
+ this.clusterView = views.clusterView;
159
+ this.clusterHullCache.clear();
160
+ }
161
+ }
162
+ measureContentHull(visual, cacheKey) {
163
+ this.contentViewByKey.set(cacheKey, this.contentView);
164
+ return measureVisualWithSvgView(
165
+ this.contentView,
166
+ visual,
167
+ cacheKey,
168
+ this.svgEl,
169
+ this.measureBin,
170
+ this.contentHullCache
171
+ );
172
+ }
173
+ measureClusterHull(visual, cacheKey) {
174
+ this.clusterViewByKey.set(cacheKey, this.clusterView);
175
+ return measureVisualWithSvgView(
176
+ this.clusterView,
177
+ visual,
178
+ cacheKey,
179
+ this.svgEl,
180
+ this.measureBin,
181
+ this.clusterHullCache
182
+ );
183
+ }
184
+ renderFrame(snapshot) {
185
+ this.svgEl.setAttribute(
186
+ "viewBox",
187
+ `0 0 ${snapshot.viewportWidth} ${snapshot.viewportHeight}`
188
+ );
189
+ this.worldGroup.setAttribute(
190
+ "transform",
191
+ `translate(${snapshot.camera.panX.toFixed(2)}, ${snapshot.camera.panY.toFixed(2)}) scale(${snapshot.camera.zoom.toFixed(4)})`
192
+ );
193
+ while (this.trianglePolygons.length < snapshot.triangles.length) {
194
+ const poly = this.doc.createElementNS(SVG_NS, "polygon");
195
+ this.trianglesGroup.appendChild(poly);
196
+ this.trianglePolygons.push(poly);
197
+ }
198
+ while (this.trianglePolygons.length > snapshot.triangles.length) {
199
+ const poly = this.trianglePolygons.pop();
200
+ poly?.remove();
201
+ }
202
+ for (let i = 0; i < snapshot.triangles.length; i++) {
203
+ const tr = snapshot.triangles[i];
204
+ const poly = this.trianglePolygons[i];
205
+ poly.setAttribute(
206
+ "points",
207
+ tr.points.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ")
208
+ );
209
+ poly.setAttribute("fill", tr.fillHex);
210
+ poly.setAttribute("fill-opacity", String(tr.fillOpacity));
211
+ }
212
+ while (this.edgeLines.length < snapshot.edges.length) {
213
+ const line = this.doc.createElementNS(SVG_NS, "line");
214
+ line.setAttribute("stroke", "rgba(148, 163, 184, 0.16)");
215
+ line.setAttribute("stroke-width", "1");
216
+ line.setAttribute("stroke-dasharray", "3 4");
217
+ this.edgesGroup.appendChild(line);
218
+ this.edgeLines.push(line);
219
+ }
220
+ while (this.edgeLines.length > snapshot.edges.length) {
221
+ const line = this.edgeLines.pop();
222
+ line?.remove();
223
+ }
224
+ for (let i = 0; i < snapshot.edges.length; i++) {
225
+ const edge = snapshot.edges[i];
226
+ const line = this.edgeLines[i];
227
+ line.setAttribute("x1", edge.a.x.toFixed(1));
228
+ line.setAttribute("y1", edge.a.y.toFixed(1));
229
+ line.setAttribute("x2", edge.b.x.toFixed(1));
230
+ line.setAttribute("y2", edge.b.y.toFixed(1));
231
+ }
232
+ this.reconcileNodes(
233
+ snapshot.clusters,
234
+ this.clustersGroup,
235
+ this.clusterDomMap,
236
+ this.clusterView,
237
+ this.clusterViewByKey,
238
+ "cluster"
239
+ );
240
+ this.reconcileNodes(
241
+ snapshot.items,
242
+ this.itemsGroup,
243
+ this.itemDomMap,
244
+ this.contentView,
245
+ this.contentViewByKey,
246
+ "item"
247
+ );
248
+ this.renderHullOverlay(snapshot);
249
+ }
250
+ reconcileNodes(nodes, parentGroup, domMap, activeView, viewByKey, kind) {
251
+ const aliveIds = /* @__PURE__ */ new Set();
252
+ for (const node of nodes) {
253
+ aliveIds.add(node.id);
254
+ let tracked = domMap.get(node.id);
255
+ if (!tracked) {
256
+ const rootGroup = this.doc.createElementNS(SVG_NS, "g");
257
+ rootGroup.setAttribute("class", `sv-node sv-node-${kind}`);
258
+ rootGroup.setAttribute("data-node-id", node.id);
259
+ parentGroup.appendChild(rootGroup);
260
+ tracked = {
261
+ rootGroup,
262
+ mainLayer: null,
263
+ fromLayer: null,
264
+ toLayer: null,
265
+ lastMainKey: null,
266
+ lastFromKey: null,
267
+ lastToKey: null,
268
+ lastInteractionSig: null,
269
+ lastMorphUsed: false
270
+ };
271
+ domMap.set(node.id, tracked);
272
+ }
273
+ const effectiveOpacity = node.interaction.isDimmed ? node.opacity * 0.24 : node.opacity;
274
+ tracked.rootGroup.setAttribute(
275
+ "transform",
276
+ `translate(${node.x.toFixed(2)}, ${node.y.toFixed(2)}) scale(${node.scale.toFixed(4)})`
277
+ );
278
+ tracked.rootGroup.setAttribute("opacity", effectiveOpacity.toFixed(3));
279
+ const interSig = interactionSignature(node);
280
+ const interactionChanged = tracked.lastInteractionSig !== interSig;
281
+ if (node.transition) {
282
+ const tr = node.transition;
283
+ const fromView = viewByKey.get(tr.fromCacheKey) ?? tracked.lastView ?? activeView;
284
+ const toView = viewByKey.get(tr.toCacheKey) ?? activeView;
285
+ if (fromView === toView && toView.morph) {
286
+ if (!tracked.mainLayer) {
287
+ tracked.mainLayer = this.createGroup("sv-layer-main");
288
+ tracked.rootGroup.appendChild(tracked.mainLayer);
289
+ }
290
+ if (tracked.fromLayer) {
291
+ tracked.fromLayer.remove();
292
+ tracked.fromLayer = null;
293
+ }
294
+ if (tracked.toLayer) {
295
+ tracked.toLayer.remove();
296
+ tracked.toLayer = null;
297
+ }
298
+ const handled = toView.morph(
299
+ tracked.mainLayer,
300
+ tr.from,
301
+ tr.to,
302
+ tr.progress,
303
+ node
304
+ );
305
+ if (handled) {
306
+ tracked.lastMorphUsed = true;
307
+ tracked.lastMainKey = null;
308
+ tracked.lastInteractionSig = interSig;
309
+ tracked.lastView = toView;
310
+ continue;
311
+ }
312
+ }
313
+ tracked.lastMorphUsed = false;
314
+ if (tracked.mainLayer) {
315
+ tracked.mainLayer.remove();
316
+ tracked.mainLayer = null;
317
+ tracked.lastMainKey = null;
318
+ }
319
+ if (!tracked.fromLayer) {
320
+ tracked.fromLayer = this.createGroup("sv-layer-from");
321
+ tracked.rootGroup.appendChild(tracked.fromLayer);
322
+ }
323
+ if (!tracked.toLayer) {
324
+ tracked.toLayer = this.createGroup("sv-layer-to");
325
+ tracked.rootGroup.appendChild(tracked.toLayer);
326
+ }
327
+ if (tracked.lastFromKey !== tr.fromCacheKey || interactionChanged) {
328
+ fromView.mountOrUpdate(tracked.fromLayer, tr.from, {
329
+ ...node,
330
+ visual: tr.from,
331
+ hull: tr.fromHull,
332
+ cacheKey: tr.fromCacheKey
333
+ });
334
+ tracked.lastFromKey = tr.fromCacheKey;
335
+ }
336
+ if (tracked.lastToKey !== tr.toCacheKey || interactionChanged) {
337
+ toView.mountOrUpdate(tracked.toLayer, tr.to, {
338
+ ...node,
339
+ visual: tr.to,
340
+ hull: tr.toHull,
341
+ cacheKey: tr.toCacheKey
342
+ });
343
+ tracked.lastToKey = tr.toCacheKey;
344
+ }
345
+ const rFrom = Math.max(1, getHullBoundingRadius(tr.fromHull));
346
+ const rTo = Math.max(1, getHullBoundingRadius(tr.toHull));
347
+ const currentR = rFrom + (rTo - rFrom) * tr.progress;
348
+ const fromSubScale = Math.max(0.4, Math.min(1.6, currentR / rFrom));
349
+ const toSubScale = Math.max(0.4, Math.min(1.6, currentR / rTo));
350
+ tracked.fromLayer.setAttribute("opacity", (1 - tr.progress).toFixed(3));
351
+ tracked.fromLayer.setAttribute("transform", `scale(${fromSubScale.toFixed(4)})`);
352
+ tracked.toLayer.setAttribute("opacity", tr.progress.toFixed(3));
353
+ tracked.toLayer.setAttribute("transform", `scale(${toSubScale.toFixed(4)})`);
354
+ tracked.lastInteractionSig = interSig;
355
+ tracked.lastView = toView;
356
+ } else {
357
+ if (tracked.fromLayer) {
358
+ tracked.fromLayer.remove();
359
+ tracked.fromLayer = null;
360
+ tracked.lastFromKey = null;
361
+ }
362
+ if (tracked.toLayer) {
363
+ if (tracked.mainLayer) tracked.mainLayer.remove();
364
+ tracked.mainLayer = tracked.toLayer;
365
+ tracked.mainLayer.setAttribute("class", "sv-layer-main");
366
+ tracked.mainLayer.removeAttribute("opacity");
367
+ tracked.mainLayer.removeAttribute("transform");
368
+ tracked.toLayer = null;
369
+ tracked.lastMainKey = tracked.lastToKey;
370
+ tracked.lastToKey = null;
371
+ }
372
+ if (!tracked.mainLayer) {
373
+ tracked.mainLayer = this.createGroup("sv-layer-main");
374
+ tracked.rootGroup.appendChild(tracked.mainLayer);
375
+ }
376
+ const viewToUse = viewByKey.get(node.cacheKey) ?? activeView;
377
+ if (tracked.lastMainKey !== node.cacheKey || interactionChanged || tracked.lastMorphUsed || tracked.lastView !== viewToUse) {
378
+ viewToUse.mountOrUpdate(tracked.mainLayer, node.visual, node);
379
+ tracked.lastMainKey = node.cacheKey;
380
+ tracked.lastInteractionSig = interSig;
381
+ tracked.lastMorphUsed = false;
382
+ tracked.lastView = viewToUse;
383
+ }
384
+ }
385
+ }
386
+ for (const [id, tracked] of domMap) {
387
+ if (!aliveIds.has(id)) {
388
+ tracked.rootGroup.remove();
389
+ domMap.delete(id);
390
+ }
391
+ }
392
+ }
393
+ renderHullOverlay(snapshot) {
394
+ while (this.hullsGroup.firstChild) {
395
+ this.hullsGroup.removeChild(this.hullsGroup.firstChild);
396
+ }
397
+ if (!snapshot.showHulls) return;
398
+ const drawHull = (node, strokeColor) => {
399
+ if (node.opacity < 0.05) return;
400
+ const hull = node.hull;
401
+ if (hull.kind === "circle") {
402
+ const circle = this.doc.createElementNS(SVG_NS, "circle");
403
+ circle.setAttribute("cx", node.x.toFixed(2));
404
+ circle.setAttribute("cy", node.y.toFixed(2));
405
+ circle.setAttribute("r", (hull.radius * node.scale).toFixed(2));
406
+ circle.setAttribute("fill", strokeColor);
407
+ circle.setAttribute("fill-opacity", "0.07");
408
+ circle.setAttribute("stroke", strokeColor);
409
+ circle.setAttribute("stroke-width", "1");
410
+ circle.setAttribute("stroke-dasharray", "3 2");
411
+ this.hullsGroup.appendChild(circle);
412
+ } else if (hull.vertices.length >= 3) {
413
+ const poly = this.doc.createElementNS(SVG_NS, "polygon");
414
+ poly.setAttribute(
415
+ "points",
416
+ hull.vertices.map(
417
+ (v) => `${(node.x + v.x * node.scale).toFixed(2)},${(node.y + v.y * node.scale).toFixed(2)}`
418
+ ).join(" ")
419
+ );
420
+ poly.setAttribute("fill", strokeColor);
421
+ poly.setAttribute("fill-opacity", "0.07");
422
+ poly.setAttribute("stroke", strokeColor);
423
+ poly.setAttribute("stroke-width", "1");
424
+ poly.setAttribute("stroke-dasharray", "3 2");
425
+ this.hullsGroup.appendChild(poly);
426
+ }
427
+ };
428
+ for (const c of snapshot.clusters) {
429
+ drawHull(c, "#f59e0b");
430
+ }
431
+ for (const it of snapshot.items) {
432
+ drawHull(it, "#22d3ee");
433
+ }
434
+ }
435
+ destroy() {
436
+ this.clusterDomMap.clear();
437
+ this.itemDomMap.clear();
438
+ this.contentHullCache.clear();
439
+ this.clusterHullCache.clear();
440
+ this.contentViewByKey.clear();
441
+ this.clusterViewByKey.clear();
442
+ while (this.svgEl.firstChild) {
443
+ this.svgEl.removeChild(this.svgEl.firstChild);
444
+ }
445
+ }
446
+ };
447
+ var SimplexSvgController = class _SimplexSvgController {
448
+ containerEl;
449
+ svgEl;
450
+ adapter;
451
+ engine;
452
+ resizeObserver = null;
453
+ onFpsUpdate;
454
+ rafId = null;
455
+ lastFrameTs = 0;
456
+ fpsSamples = 0;
457
+ fpsAccumMs = 0;
458
+ destroyed = false;
459
+ // Centralized drag-to-pan state on root <svg>
460
+ isDraggingPan = false;
461
+ dragStartX = 0;
462
+ dragStartY = 0;
463
+ dragMoved = false;
464
+ constructor(containerEl, options) {
465
+ this.containerEl = containerEl;
466
+ this.onFpsUpdate = options.onFpsUpdate;
467
+ const doc = containerEl.ownerDocument;
468
+ this.svgEl = doc.createElementNS(SVG_NS, "svg");
469
+ this.svgEl.setAttribute("width", "100%");
470
+ this.svgEl.setAttribute("height", "100%");
471
+ this.svgEl.style.display = "block";
472
+ this.svgEl.style.touchAction = "none";
473
+ this.svgEl.style.userSelect = "none";
474
+ containerEl.appendChild(this.svgEl);
475
+ const rect = containerEl.getBoundingClientRect();
476
+ const initialW = rect.width > 40 ? rect.width : 960;
477
+ const initialH = rect.height > 40 ? rect.height : 620;
478
+ this.adapter = new SvgRendererAdapter(this.svgEl, {
479
+ contentView: options.contentView,
480
+ clusterView: options.clusterView
481
+ });
482
+ this.engine = SimplexGraphEngine.createFromResolved({
483
+ items: options.items,
484
+ getId: options.getId,
485
+ getTags: options.getTags,
486
+ deriveClusterMeta: options.deriveClusterMeta,
487
+ visualResolver: options.visualResolver,
488
+ renderer: this.adapter,
489
+ viewportWidth: initialW,
490
+ viewportHeight: initialH,
491
+ normalizeTag: options.normalizeTag,
492
+ resolveTagMeta: options.resolveTagMeta,
493
+ meshOptions: options.meshOptions,
494
+ layoutOptions: options.layoutOptions,
495
+ showHulls: options.showHulls,
496
+ interactions: options.interactions,
497
+ onMetricsChange: options.onMetricsChange,
498
+ onClustersChange: options.onClustersChange
499
+ });
500
+ this.attachSvgListeners();
501
+ if (typeof ResizeObserver !== "undefined") {
502
+ this.resizeObserver = new ResizeObserver((entries) => {
503
+ if (this.destroyed) return;
504
+ for (const entry of entries) {
505
+ const { width, height } = entry.contentRect;
506
+ if (width > 60 && height > 60) {
507
+ this.engine.setViewport(width, height);
508
+ this.ensureLoopRunning();
509
+ }
510
+ }
511
+ });
512
+ this.resizeObserver.observe(containerEl);
513
+ }
514
+ this.ensureLoopRunning();
515
+ }
516
+ static createFromResolved(containerEl, options) {
517
+ return new _SimplexSvgController(containerEl, options);
518
+ }
519
+ get camera() {
520
+ return this.engine.camera;
521
+ }
522
+ getSvgElement() {
523
+ return this.svgEl;
524
+ }
525
+ getMetrics() {
526
+ return this.engine.getMetrics();
527
+ }
528
+ getClusters() {
529
+ return this.engine.getClusters();
530
+ }
531
+ getItems() {
532
+ return this.engine.getItems();
533
+ }
534
+ addItems(items) {
535
+ this.engine.addItems(items);
536
+ this.ensureLoopRunning();
537
+ }
538
+ removeItems(ids) {
539
+ this.engine.removeItems(ids);
540
+ this.ensureLoopRunning();
541
+ }
542
+ setItems(items) {
543
+ this.engine.setItems(items);
544
+ this.ensureLoopRunning();
545
+ }
546
+ setVisualResolver(resolver) {
547
+ this.engine.setVisualResolver(resolver);
548
+ this.ensureLoopRunning();
549
+ }
550
+ setViews(views) {
551
+ this.adapter.setViews(views);
552
+ }
553
+ setSizeMultiplier(multiplier) {
554
+ this.engine.setSizeMultiplier(multiplier);
555
+ this.ensureLoopRunning();
556
+ }
557
+ setShowHulls(show) {
558
+ this.engine.setShowHulls(show);
559
+ this.ensureLoopRunning();
560
+ }
561
+ setInteractions(interactions) {
562
+ this.engine.setInteractions(interactions);
563
+ }
564
+ setPinnedIds(ids) {
565
+ this.engine.setPinnedIds(ids);
566
+ this.ensureLoopRunning();
567
+ }
568
+ setHighlightedIds(ids) {
569
+ this.engine.setHighlightedIds(ids);
570
+ this.ensureLoopRunning();
571
+ }
572
+ setFocusedClusterUid(clusterUid) {
573
+ this.engine.setFocusedClusterUid(clusterUid);
574
+ this.ensureLoopRunning();
575
+ }
576
+ /** Deterministically advances simulation time in tests without waiting for rAF. */
577
+ stepForTest(dtMs) {
578
+ return this.engine.step(dtMs);
579
+ }
580
+ toLocalCoords(clientX, clientY) {
581
+ const rect = this.svgEl.getBoundingClientRect();
582
+ return {
583
+ x: clientX - rect.left,
584
+ y: clientY - rect.top
585
+ };
586
+ }
587
+ onPointerDown = (e) => {
588
+ if (e.button !== 0) return;
589
+ const { x, y } = this.toLocalCoords(e.clientX, e.clientY);
590
+ const hit = this.engine.hitTest(x, y);
591
+ if (hit.kind === "background") {
592
+ this.isDraggingPan = true;
593
+ this.dragStartX = e.clientX;
594
+ this.dragStartY = e.clientY;
595
+ this.dragMoved = false;
596
+ this.svgEl.style.cursor = "grabbing";
597
+ } else {
598
+ this.isDraggingPan = false;
599
+ this.dragMoved = false;
600
+ }
601
+ };
602
+ onPointerMove = (e) => {
603
+ const { x, y } = this.toLocalCoords(e.clientX, e.clientY);
604
+ if (this.isDraggingPan) {
605
+ const dx = e.clientX - this.dragStartX;
606
+ const dy = e.clientY - this.dragStartY;
607
+ if (Math.hypot(dx, dy) > 3) {
608
+ this.dragMoved = true;
609
+ }
610
+ if (this.dragMoved) {
611
+ this.dragStartX = e.clientX;
612
+ this.dragStartY = e.clientY;
613
+ this.engine.camera.panBy(dx, dy);
614
+ this.ensureLoopRunning();
615
+ }
616
+ return;
617
+ }
618
+ this.engine.handlePointerMove(x, y);
619
+ const hit = this.engine.hitTest(x, y);
620
+ this.svgEl.style.cursor = hit.kind === "background" ? "grab" : "pointer";
621
+ this.ensureLoopRunning();
622
+ };
623
+ onPointerUp = () => {
624
+ if (this.isDraggingPan) {
625
+ this.isDraggingPan = false;
626
+ this.svgEl.style.cursor = "grab";
627
+ }
628
+ };
629
+ onPointerLeave = () => {
630
+ this.isDraggingPan = false;
631
+ this.engine.handlePointerLeave();
632
+ this.ensureLoopRunning();
633
+ };
634
+ onClick = (e) => {
635
+ if (this.dragMoved) {
636
+ this.dragMoved = false;
637
+ return;
638
+ }
639
+ const { x, y } = this.toLocalCoords(e.clientX, e.clientY);
640
+ this.engine.handleLeftClick({
641
+ screenX: x,
642
+ screenY: y,
643
+ button: e.button,
644
+ shiftKey: e.shiftKey,
645
+ ctrlKey: e.ctrlKey,
646
+ altKey: e.altKey,
647
+ metaKey: e.metaKey
648
+ });
649
+ this.ensureLoopRunning();
650
+ };
651
+ onContextMenu = (e) => {
652
+ const { x, y } = this.toLocalCoords(e.clientX, e.clientY);
653
+ const suppress = this.engine.handleRightClick({
654
+ screenX: x,
655
+ screenY: y,
656
+ button: e.button,
657
+ shiftKey: e.shiftKey,
658
+ ctrlKey: e.ctrlKey,
659
+ altKey: e.altKey,
660
+ metaKey: e.metaKey
661
+ });
662
+ if (suppress) {
663
+ e.preventDefault();
664
+ }
665
+ this.ensureLoopRunning();
666
+ };
667
+ onWheel = (e) => {
668
+ const { x, y } = this.toLocalCoords(e.clientX, e.clientY);
669
+ const handled = this.engine.handleWheel({
670
+ screenX: x,
671
+ screenY: y,
672
+ deltaX: e.deltaX,
673
+ deltaY: e.deltaY,
674
+ ctrlKey: e.ctrlKey,
675
+ shiftKey: e.shiftKey
676
+ });
677
+ if (handled) {
678
+ e.preventDefault();
679
+ this.ensureLoopRunning();
680
+ }
681
+ };
682
+ attachSvgListeners() {
683
+ this.svgEl.addEventListener("pointerdown", this.onPointerDown);
684
+ this.svgEl.addEventListener("pointermove", this.onPointerMove);
685
+ this.svgEl.addEventListener("pointerup", this.onPointerUp);
686
+ this.svgEl.addEventListener("pointerleave", this.onPointerLeave);
687
+ this.svgEl.addEventListener("click", this.onClick);
688
+ this.svgEl.addEventListener("contextmenu", this.onContextMenu);
689
+ this.svgEl.addEventListener("wheel", this.onWheel, { passive: false });
690
+ }
691
+ detachSvgListeners() {
692
+ this.svgEl.removeEventListener("pointerdown", this.onPointerDown);
693
+ this.svgEl.removeEventListener("pointermove", this.onPointerMove);
694
+ this.svgEl.removeEventListener("pointerup", this.onPointerUp);
695
+ this.svgEl.removeEventListener("pointerleave", this.onPointerLeave);
696
+ this.svgEl.removeEventListener("click", this.onClick);
697
+ this.svgEl.removeEventListener("contextmenu", this.onContextMenu);
698
+ this.svgEl.removeEventListener("wheel", this.onWheel);
699
+ }
700
+ ensureLoopRunning() {
701
+ if (this.destroyed || this.rafId !== null) return;
702
+ if (typeof requestAnimationFrame === "undefined") {
703
+ this.engine.renderNow();
704
+ return;
705
+ }
706
+ this.lastFrameTs = performance.now();
707
+ const tick = (now) => {
708
+ if (this.destroyed) return;
709
+ const dtMs = Math.max(1, Math.min(50, now - this.lastFrameTs));
710
+ this.lastFrameTs = now;
711
+ this.fpsSamples += 1;
712
+ this.fpsAccumMs += dtMs;
713
+ if (this.fpsAccumMs >= 320) {
714
+ const fps = Math.round(this.fpsSamples * 1e3 / this.fpsAccumMs);
715
+ this.onFpsUpdate?.(fps);
716
+ this.fpsSamples = 0;
717
+ this.fpsAccumMs = 0;
718
+ }
719
+ const settled = this.engine.step(dtMs);
720
+ if (!settled) {
721
+ this.rafId = requestAnimationFrame(tick);
722
+ } else {
723
+ this.rafId = null;
724
+ }
725
+ };
726
+ this.rafId = requestAnimationFrame(tick);
727
+ }
728
+ destroy() {
729
+ if (this.destroyed) return;
730
+ this.destroyed = true;
731
+ if (this.rafId !== null && typeof cancelAnimationFrame !== "undefined") {
732
+ cancelAnimationFrame(this.rafId);
733
+ this.rafId = null;
734
+ }
735
+ this.resizeObserver?.disconnect();
736
+ this.detachSvgListeners();
737
+ this.engine.destroy();
738
+ if (this.svgEl.parentElement === this.containerEl) {
739
+ this.containerEl.removeChild(this.svgEl);
740
+ }
741
+ }
742
+ };
743
+ var MORPH_SAMPLES = 64;
744
+ function sampleShapePerimeter(shape, radius) {
745
+ const r = Math.max(3, radius);
746
+ const pts = [];
747
+ if (shape === "circle") {
748
+ for (let i = 0; i < MORPH_SAMPLES; i++) {
749
+ const a = i / MORPH_SAMPLES * Math.PI * 2 - Math.PI / 2;
750
+ pts.push({ x: Math.cos(a) * r, y: Math.sin(a) * r });
751
+ }
752
+ return pts;
753
+ }
754
+ const polyVerts = [];
755
+ if (shape === "diamond") {
756
+ const d = r * 1.12;
757
+ polyVerts.push(
758
+ { x: 0, y: -d },
759
+ { x: d * 0.88, y: 0 },
760
+ { x: 0, y: d },
761
+ { x: -d * 0.88, y: 0 }
762
+ );
763
+ } else if (shape === "hexagon") {
764
+ for (let i = 0; i < 6; i++) {
765
+ const a = -Math.PI / 2 + i * Math.PI * 2 / 6;
766
+ polyVerts.push({ x: Math.cos(a) * r * 1.05, y: Math.sin(a) * r * 1.05 });
767
+ }
768
+ } else {
769
+ const outer = r * 1.24;
770
+ const inner = r * 0.56;
771
+ for (let i = 0; i < 10; i++) {
772
+ const a = -Math.PI / 2 + i * Math.PI / 5;
773
+ const rad = i % 2 === 0 ? outer : inner;
774
+ polyVerts.push({ x: Math.cos(a) * rad, y: Math.sin(a) * rad });
775
+ }
776
+ }
777
+ const edgeLens = [];
778
+ let totalLen = 0;
779
+ for (let i = 0; i < polyVerts.length; i++) {
780
+ const a = polyVerts[i];
781
+ const b = polyVerts[(i + 1) % polyVerts.length];
782
+ const len = Math.hypot(b.x - a.x, b.y - a.y);
783
+ edgeLens.push(len);
784
+ totalLen += len;
785
+ }
786
+ for (let s = 0; s < MORPH_SAMPLES; s++) {
787
+ const targetDist = s / MORPH_SAMPLES * totalLen;
788
+ let accum = 0;
789
+ for (let i = 0; i < polyVerts.length; i++) {
790
+ const len = edgeLens[i];
791
+ if (accum + len >= targetDist || i === polyVerts.length - 1) {
792
+ const localT = len > 1e-6 ? (targetDist - accum) / len : 0;
793
+ const a = polyVerts[i];
794
+ const b = polyVerts[(i + 1) % polyVerts.length];
795
+ pts.push({
796
+ x: a.x + (b.x - a.x) * localT,
797
+ y: a.y + (b.y - a.y) * localT
798
+ });
799
+ break;
800
+ }
801
+ accum += len;
802
+ }
803
+ }
804
+ return pts;
805
+ }
806
+ function pointsToSvgPath(pts) {
807
+ if (pts.length === 0) return "";
808
+ let d = `M ${pts[0].x.toFixed(2)} ${pts[0].y.toFixed(2)}`;
809
+ for (let i = 1; i < pts.length; i++) {
810
+ const p = pts[i];
811
+ d += ` L ${p.x.toFixed(2)} ${p.y.toFixed(2)}`;
812
+ }
813
+ return `${d} Z`;
814
+ }
815
+ function parseHexRgb(hex) {
816
+ const clean = hex.startsWith("#") ? hex.slice(1) : hex;
817
+ if (clean.length < 6) return [148, 163, 184];
818
+ return [
819
+ parseInt(clean.slice(0, 2), 16) || 0,
820
+ parseInt(clean.slice(2, 4), 16) || 0,
821
+ parseInt(clean.slice(4, 6), 16) || 0
822
+ ];
823
+ }
824
+ function lerpHex(fromHex, toHex, t) {
825
+ const a = parseHexRgb(fromHex);
826
+ const b = parseHexRgb(toHex);
827
+ const r = Math.round(a[0] + (b[0] - a[0]) * t);
828
+ const g = Math.round(a[1] + (b[1] - a[1]) * t);
829
+ const bl = Math.round(a[2] + (b[2] - a[2]) * t);
830
+ return `rgb(${r}, ${g}, ${bl})`;
831
+ }
832
+ var GeometricShapeSvgView = class {
833
+ measureConvexHull(visual) {
834
+ const r = visual.radius + (visual.strokeWidth ?? 1.5);
835
+ if (visual.shape === "circle") {
836
+ return { kind: "circle", radius: r };
837
+ }
838
+ if (visual.shape === "diamond") {
839
+ const d = r * 1.12;
840
+ return {
841
+ kind: "polygon",
842
+ vertices: computeConvexHull([
843
+ { x: 0, y: -d },
844
+ { x: d * 0.88, y: 0 },
845
+ { x: 0, y: d },
846
+ { x: -d * 0.88, y: 0 }
847
+ ])
848
+ };
849
+ }
850
+ if (visual.shape === "hexagon") {
851
+ return regularPolygon(6, r * 1.05);
852
+ }
853
+ return starConvexHull(5, r * 1.24);
854
+ }
855
+ ensureElements(groupEl) {
856
+ let pathEl = groupEl.querySelector("path.sv-geom-shape");
857
+ let ringEl = groupEl.querySelector("path.sv-geom-ring");
858
+ if (!pathEl || !ringEl) {
859
+ while (groupEl.firstChild) groupEl.removeChild(groupEl.firstChild);
860
+ const doc = groupEl.ownerDocument;
861
+ ringEl = doc.createElementNS(SVG_NS, "path");
862
+ ringEl.setAttribute("class", "sv-geom-ring");
863
+ ringEl.setAttribute("fill", "none");
864
+ pathEl = doc.createElementNS(SVG_NS, "path");
865
+ pathEl.setAttribute("class", "sv-geom-shape");
866
+ groupEl.appendChild(ringEl);
867
+ groupEl.appendChild(pathEl);
868
+ }
869
+ return { pathEl, ringEl };
870
+ }
871
+ mountOrUpdate(groupEl, visual, state) {
872
+ const { pathEl, ringEl } = this.ensureElements(groupEl);
873
+ const pts = sampleShapePerimeter(visual.shape, visual.radius);
874
+ const d = pointsToSvgPath(pts);
875
+ pathEl.setAttribute("d", d);
876
+ pathEl.setAttribute("fill", visual.fillHex);
877
+ pathEl.setAttribute(
878
+ "stroke",
879
+ state.interaction.isPinned ? "#f59e0b" : state.interaction.isHovered ? "#f8fafc" : visual.strokeHex ?? "rgba(15, 23, 42, 0.8)"
880
+ );
881
+ pathEl.setAttribute(
882
+ "stroke-width",
883
+ String(state.interaction.isHovered || state.interaction.isPinned ? 2.2 : visual.strokeWidth ?? 1.4)
884
+ );
885
+ if (state.interaction.isHovered || state.interaction.isPinned || state.interaction.isHighlighted) {
886
+ const outerPts = sampleShapePerimeter(visual.shape, visual.radius + 4.5);
887
+ ringEl.setAttribute("d", pointsToSvgPath(outerPts));
888
+ ringEl.setAttribute("stroke", state.interaction.isPinned ? "#f59e0b" : "#22d3ee");
889
+ ringEl.setAttribute("stroke-width", "1.4");
890
+ ringEl.setAttribute("stroke-opacity", "0.9");
891
+ } else {
892
+ ringEl.setAttribute("d", "");
893
+ }
894
+ }
895
+ morph(groupEl, from, to, progress, state) {
896
+ const { pathEl, ringEl } = this.ensureElements(groupEl);
897
+ const ptsA = sampleShapePerimeter(from.shape, from.radius);
898
+ const ptsB = sampleShapePerimeter(to.shape, to.radius);
899
+ const morphed = [];
900
+ for (let i = 0; i < MORPH_SAMPLES; i++) {
901
+ const a = ptsA[i];
902
+ const b = ptsB[i];
903
+ morphed.push({
904
+ x: a.x + (b.x - a.x) * progress,
905
+ y: a.y + (b.y - a.y) * progress
906
+ });
907
+ }
908
+ pathEl.setAttribute("d", pointsToSvgPath(morphed));
909
+ pathEl.setAttribute("fill", lerpHex(from.fillHex, to.fillHex, progress));
910
+ pathEl.setAttribute(
911
+ "stroke",
912
+ state.interaction.isHovered ? "#f8fafc" : to.strokeHex ?? "rgba(15, 23, 42, 0.8)"
913
+ );
914
+ pathEl.setAttribute("stroke-width", String(to.strokeWidth ?? 1.4));
915
+ ringEl.setAttribute("d", "");
916
+ return true;
917
+ }
918
+ };
919
+
920
+ // src/views/MultiSectorGlyphSvgView.ts
921
+ function describeDonutSectorPath(innerR, outerR, startAngle, endAngle) {
922
+ const sweep = endAngle - startAngle;
923
+ if (sweep >= Math.PI * 2 - 1e-4) {
924
+ return [
925
+ `M ${outerR} 0`,
926
+ `A ${outerR} ${outerR} 0 1 0 ${-outerR} 0`,
927
+ `A ${outerR} ${outerR} 0 1 0 ${outerR} 0`,
928
+ `M ${innerR} 0`,
929
+ `A ${innerR} ${innerR} 0 1 1 ${-innerR} 0`,
930
+ `A ${innerR} ${innerR} 0 1 1 ${innerR} 0`,
931
+ "Z"
932
+ ].join(" ");
933
+ }
934
+ const largeArc = sweep > Math.PI ? 1 : 0;
935
+ const x1o = Math.cos(startAngle) * outerR;
936
+ const y1o = Math.sin(startAngle) * outerR;
937
+ const x2o = Math.cos(endAngle) * outerR;
938
+ const y2o = Math.sin(endAngle) * outerR;
939
+ const x1i = Math.cos(endAngle) * innerR;
940
+ const y1i = Math.sin(endAngle) * innerR;
941
+ const x2i = Math.cos(startAngle) * innerR;
942
+ const y2i = Math.sin(startAngle) * innerR;
943
+ return [
944
+ `M ${x1o.toFixed(2)} ${y1o.toFixed(2)}`,
945
+ `A ${outerR.toFixed(2)} ${outerR.toFixed(2)} 0 ${largeArc} 1 ${x2o.toFixed(2)} ${y2o.toFixed(2)}`,
946
+ `L ${x1i.toFixed(2)} ${y1i.toFixed(2)}`,
947
+ `A ${innerR.toFixed(2)} ${innerR.toFixed(2)} 0 ${largeArc} 0 ${x2i.toFixed(2)} ${y2i.toFixed(2)}`,
948
+ "Z"
949
+ ].join(" ");
950
+ }
951
+ var MultiSectorGlyphSvgView = class {
952
+ measureConvexHull(visual) {
953
+ return { kind: "circle", radius: visual.outerRadius + 2 };
954
+ }
955
+ mountOrUpdate(groupEl, visual, state) {
956
+ const doc = groupEl.ownerDocument;
957
+ while (groupEl.firstChild) {
958
+ groupEl.removeChild(groupEl.firstChild);
959
+ }
960
+ const totalWeight = visual.sectors.reduce((s, sec) => s + Math.max(0, sec.weight), 0) || 1;
961
+ let angle = -Math.PI / 2;
962
+ for (const sec of visual.sectors) {
963
+ const frac = Math.max(0, sec.weight) / totalWeight;
964
+ if (frac <= 5e-3) continue;
965
+ const nextAngle = angle + frac * Math.PI * 2;
966
+ const path = doc.createElementNS(SVG_NS, "path");
967
+ path.setAttribute(
968
+ "d",
969
+ describeDonutSectorPath(visual.innerRadius, visual.outerRadius, angle, nextAngle)
970
+ );
971
+ path.setAttribute("fill", sec.colorHex);
972
+ path.setAttribute("stroke", "#07090E");
973
+ path.setAttribute("stroke-width", visual.sectors.length > 1 ? "1.1" : "0.8");
974
+ groupEl.appendChild(path);
975
+ angle = nextAngle;
976
+ }
977
+ const core = doc.createElementNS(SVG_NS, "circle");
978
+ core.setAttribute("cx", "0");
979
+ core.setAttribute("cy", "0");
980
+ core.setAttribute("r", String(Math.max(2, visual.innerRadius - 1)));
981
+ core.setAttribute("fill", visual.centerColorHex);
982
+ groupEl.appendChild(core);
983
+ if (visual.accentRingHex || state.interaction.isHovered || state.interaction.isPinned || state.interaction.isHighlighted) {
984
+ const ring = doc.createElementNS(SVG_NS, "circle");
985
+ ring.setAttribute("cx", "0");
986
+ ring.setAttribute("cy", "0");
987
+ ring.setAttribute("r", String(visual.outerRadius + 3));
988
+ ring.setAttribute("fill", "none");
989
+ ring.setAttribute(
990
+ "stroke",
991
+ state.interaction.isPinned ? "#f59e0b" : state.interaction.isHovered ? "#f8fafc" : visual.accentRingHex ?? "#22d3ee"
992
+ );
993
+ ring.setAttribute("stroke-width", state.interaction.isHovered ? "2" : "1.2");
994
+ groupEl.appendChild(ring);
995
+ }
996
+ }
997
+ };
998
+
999
+ // src/views/BadgeClusterSvgView.ts
1000
+ var BadgeClusterSvgView = class {
1001
+ mountOrUpdate(groupEl, visual, state) {
1002
+ const doc = groupEl.ownerDocument;
1003
+ while (groupEl.firstChild) {
1004
+ groupEl.removeChild(groupEl.firstChild);
1005
+ }
1006
+ const titleLen = visual.title.length;
1007
+ const metaLen = visual.metaLine.length;
1008
+ const estWidth = Math.max(122, Math.max(titleLen * 6.8 + 44, metaLen * 5.9 + 24));
1009
+ const height = 42;
1010
+ const halfW = estWidth / 2;
1011
+ const bg = doc.createElementNS(SVG_NS, "rect");
1012
+ bg.setAttribute("x", String(-halfW));
1013
+ bg.setAttribute("y", String(-21));
1014
+ bg.setAttribute("width", String(estWidth));
1015
+ bg.setAttribute("height", String(height));
1016
+ bg.setAttribute("rx", "8");
1017
+ bg.setAttribute("fill", "#0d1320");
1018
+ bg.setAttribute("fill-opacity", "0.94");
1019
+ bg.setAttribute(
1020
+ "stroke",
1021
+ state.interaction.isPinned ? "#f59e0b" : state.interaction.isHovered || state.interaction.isHighlighted ? "#e2e8f0" : visual.colorHex
1022
+ );
1023
+ bg.setAttribute(
1024
+ "stroke-width",
1025
+ state.interaction.isHovered || state.interaction.isHighlighted ? "2.2" : "1.4"
1026
+ );
1027
+ groupEl.appendChild(bg);
1028
+ const swatch = doc.createElementNS(SVG_NS, "circle");
1029
+ swatch.setAttribute("cx", String(-halfW + 13));
1030
+ swatch.setAttribute("cy", String(-7));
1031
+ swatch.setAttribute("r", "4");
1032
+ swatch.setAttribute("fill", visual.colorHex);
1033
+ groupEl.appendChild(swatch);
1034
+ const codeText = doc.createElementNS(SVG_NS, "text");
1035
+ codeText.setAttribute("x", String(-halfW + 22));
1036
+ codeText.setAttribute("y", String(-3));
1037
+ codeText.setAttribute("fill", visual.colorHex);
1038
+ codeText.setAttribute("font-size", "10");
1039
+ codeText.setAttribute("font-weight", "600");
1040
+ codeText.setAttribute("font-family", "IBM Plex Mono, monospace");
1041
+ codeText.textContent = visual.code;
1042
+ groupEl.appendChild(codeText);
1043
+ const titleText = doc.createElementNS(SVG_NS, "text");
1044
+ titleText.setAttribute("x", String(-halfW + 44));
1045
+ titleText.setAttribute("y", String(-3));
1046
+ titleText.setAttribute("fill", "#f1f5f9");
1047
+ titleText.setAttribute("font-size", "11");
1048
+ titleText.setAttribute("font-weight", "600");
1049
+ titleText.setAttribute("font-family", "Plus Jakarta Sans, sans-serif");
1050
+ titleText.textContent = visual.title;
1051
+ groupEl.appendChild(titleText);
1052
+ const subText = doc.createElementNS(SVG_NS, "text");
1053
+ subText.setAttribute("x", "0");
1054
+ subText.setAttribute("y", "13");
1055
+ subText.setAttribute("text-anchor", "middle");
1056
+ subText.setAttribute("fill", "#94a3b8");
1057
+ subText.setAttribute("font-size", "9.5");
1058
+ subText.setAttribute("font-family", "IBM Plex Mono, monospace");
1059
+ subText.textContent = visual.metaLine;
1060
+ groupEl.appendChild(subText);
1061
+ }
1062
+ };
1063
+
1064
+ // src/views/ClusterAnchorSvgView.ts
1065
+ var ClusterAnchorSvgView = class {
1066
+ measureConvexHull(visual) {
1067
+ return { kind: "circle", radius: visual.radius + 5 };
1068
+ }
1069
+ mountOrUpdate(groupEl, visual, state) {
1070
+ const doc = groupEl.ownerDocument;
1071
+ while (groupEl.firstChild) {
1072
+ groupEl.removeChild(groupEl.firstChild);
1073
+ }
1074
+ const r = visual.radius;
1075
+ const outer = doc.createElementNS(SVG_NS, "circle");
1076
+ outer.setAttribute("cx", "0");
1077
+ outer.setAttribute("cy", "0");
1078
+ outer.setAttribute("r", String(r + 3));
1079
+ outer.setAttribute("fill", "#07090E");
1080
+ outer.setAttribute("fill-opacity", "0.88");
1081
+ outer.setAttribute(
1082
+ "stroke",
1083
+ state.interaction.isHovered || state.interaction.isHighlighted ? "#f8fafc" : visual.colorHex
1084
+ );
1085
+ outer.setAttribute("stroke-width", state.interaction.isHovered ? "2.4" : "1.6");
1086
+ outer.setAttribute("stroke-dasharray", "4 2.5");
1087
+ groupEl.appendChild(outer);
1088
+ const inner = doc.createElementNS(SVG_NS, "circle");
1089
+ inner.setAttribute("cx", "0");
1090
+ inner.setAttribute("cy", "0");
1091
+ inner.setAttribute("r", String(r - 2));
1092
+ inner.setAttribute("fill", visual.colorHex);
1093
+ inner.setAttribute("fill-opacity", "0.18");
1094
+ inner.setAttribute("stroke", visual.colorHex);
1095
+ inner.setAttribute("stroke-width", "1.4");
1096
+ groupEl.appendChild(inner);
1097
+ const codeEl = doc.createElementNS(SVG_NS, "text");
1098
+ codeEl.setAttribute("x", "0");
1099
+ codeEl.setAttribute("y", "-2");
1100
+ codeEl.setAttribute("text-anchor", "middle");
1101
+ codeEl.setAttribute("fill", "#f8fafc");
1102
+ codeEl.setAttribute("font-size", "11");
1103
+ codeEl.setAttribute("font-weight", "600");
1104
+ codeEl.setAttribute("font-family", "IBM Plex Mono, monospace");
1105
+ codeEl.textContent = visual.code;
1106
+ groupEl.appendChild(codeEl);
1107
+ const countEl = doc.createElementNS(SVG_NS, "text");
1108
+ countEl.setAttribute("x", "0");
1109
+ countEl.setAttribute("y", "11");
1110
+ countEl.setAttribute("text-anchor", "middle");
1111
+ countEl.setAttribute("fill", visual.colorHex);
1112
+ countEl.setAttribute("font-size", "9");
1113
+ countEl.setAttribute("font-family", "IBM Plex Mono, monospace");
1114
+ countEl.textContent = `${visual.memberCount}n`;
1115
+ groupEl.appendChild(countEl);
1116
+ }
1117
+ };
1118
+
1119
+ export { BadgeClusterSvgView, ClusterAnchorSvgView, GeometricShapeSvgView, MultiSectorGlyphSvgView, SVG_NS, SimplexSvgController, SvgRendererAdapter, measureVisualWithSvgView };
1120
+ //# sourceMappingURL=index.js.map
1121
+ //# sourceMappingURL=index.js.map