@simplex-viz/svg 0.1.3 → 0.2.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 CHANGED
@@ -1,4 +1,5 @@
1
- import { rectToConvexPolygon, getHullBoundingRadius, SimplexGraphEngine, computeConvexHull, regularPolygon, starConvexHull } from '@simplex-viz/core';
1
+ import { rectToConvexPolygon, computeConvexHull, regularPolygon, starConvexHull, getHullBoundingRadius, SimplexGraphEngine } from '@simplex-viz/core';
2
+ export { computeConvexHull, rectToConvexPolygon, regularPolygon, solveSimplexSnapshot, starConvexHull } from '@simplex-viz/core';
2
3
 
3
4
  // src/hullMeasure.ts
4
5
  var SVG_NS = "http://www.w3.org/2000/svg";
@@ -89,204 +90,664 @@ function measureVisualWithSvgView(view, visual, cacheKey, rootSvg, measureBin, c
89
90
  cache.set(cacheKey, hull);
90
91
  return hull;
91
92
  }
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);
93
+
94
+ // src/views/BadgeClusterSvgView.ts
95
+ var BadgeClusterSvgView = class {
96
+ mountOrUpdate(groupEl, visual, state) {
97
+ const doc = groupEl.ownerDocument;
98
+ while (groupEl.firstChild) {
99
+ groupEl.removeChild(groupEl.firstChild);
123
100
  }
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);
101
+ const titleLen = visual.title.length;
102
+ const metaLen = visual.metaLine.length;
103
+ const estWidth = Math.max(122, Math.max(titleLen * 6.8 + 44, metaLen * 5.9 + 24));
104
+ const height = 42;
105
+ const halfW = estWidth / 2;
106
+ const bg = doc.createElementNS(SVG_NS, "rect");
107
+ bg.setAttribute("x", String(-halfW));
108
+ bg.setAttribute("y", String(-21));
109
+ bg.setAttribute("width", String(estWidth));
110
+ bg.setAttribute("height", String(height));
111
+ bg.setAttribute("rx", "8");
112
+ bg.setAttribute("fill", "#0d1320");
113
+ bg.setAttribute("fill-opacity", "0.94");
114
+ bg.setAttribute(
115
+ "stroke",
116
+ state.interaction.isPinned ? "#f59e0b" : state.interaction.isHovered || state.interaction.isHighlighted ? "#e2e8f0" : visual.colorHex
117
+ );
118
+ bg.setAttribute(
119
+ "stroke-width",
120
+ state.interaction.isHovered || state.interaction.isHighlighted ? "2.2" : "1.4"
121
+ );
122
+ groupEl.appendChild(bg);
123
+ const swatch = doc.createElementNS(SVG_NS, "circle");
124
+ swatch.setAttribute("cx", String(-halfW + 13));
125
+ swatch.setAttribute("cy", String(-7));
126
+ swatch.setAttribute("r", "4");
127
+ swatch.setAttribute("fill", visual.colorHex);
128
+ groupEl.appendChild(swatch);
129
+ const codeText = doc.createElementNS(SVG_NS, "text");
130
+ codeText.setAttribute("x", String(-halfW + 22));
131
+ codeText.setAttribute("y", String(-3));
132
+ codeText.setAttribute("fill", visual.colorHex);
133
+ codeText.setAttribute("font-size", "10");
134
+ codeText.setAttribute("font-weight", "600");
135
+ codeText.setAttribute("font-family", "IBM Plex Mono, monospace");
136
+ codeText.textContent = visual.code;
137
+ groupEl.appendChild(codeText);
138
+ const titleText = doc.createElementNS(SVG_NS, "text");
139
+ titleText.setAttribute("x", String(-halfW + 44));
140
+ titleText.setAttribute("y", String(-3));
141
+ titleText.setAttribute("fill", "#f1f5f9");
142
+ titleText.setAttribute("font-size", "11");
143
+ titleText.setAttribute("font-weight", "600");
144
+ titleText.setAttribute("font-family", "Plus Jakarta Sans, sans-serif");
145
+ titleText.textContent = visual.title;
146
+ groupEl.appendChild(titleText);
147
+ const subText = doc.createElementNS(SVG_NS, "text");
148
+ subText.setAttribute("x", "0");
149
+ subText.setAttribute("y", "13");
150
+ subText.setAttribute("text-anchor", "middle");
151
+ subText.setAttribute("fill", "#94a3b8");
152
+ subText.setAttribute("font-size", "9.5");
153
+ subText.setAttribute("font-family", "IBM Plex Mono, monospace");
154
+ subText.textContent = visual.metaLine;
155
+ groupEl.appendChild(subText);
142
156
  }
143
- createGroup(className) {
144
- const g = this.doc.createElementNS(SVG_NS, "g");
145
- g.setAttribute("class", className);
146
- return g;
157
+ };
158
+
159
+ // src/views/ClusterAnchorSvgView.ts
160
+ var ClusterAnchorSvgView = class {
161
+ measureConvexHull(visual) {
162
+ return { kind: "circle", radius: visual.radius + 5 };
147
163
  }
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();
164
+ mountOrUpdate(groupEl, visual, state) {
165
+ const doc = groupEl.ownerDocument;
166
+ while (groupEl.firstChild) {
167
+ groupEl.removeChild(groupEl.firstChild);
160
168
  }
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
169
+ const r = visual.radius;
170
+ const outer = doc.createElementNS(SVG_NS, "circle");
171
+ outer.setAttribute("cx", "0");
172
+ outer.setAttribute("cy", "0");
173
+ outer.setAttribute("r", String(r + 3));
174
+ outer.setAttribute("fill", "#07090E");
175
+ outer.setAttribute("fill-opacity", "0.88");
176
+ outer.setAttribute(
177
+ "stroke",
178
+ state.interaction.isHovered || state.interaction.isHighlighted ? "#f8fafc" : visual.colorHex
171
179
  );
180
+ outer.setAttribute("stroke-width", state.interaction.isHovered ? "2.4" : "1.6");
181
+ outer.setAttribute("stroke-dasharray", "4 2.5");
182
+ groupEl.appendChild(outer);
183
+ const inner = doc.createElementNS(SVG_NS, "circle");
184
+ inner.setAttribute("cx", "0");
185
+ inner.setAttribute("cy", "0");
186
+ inner.setAttribute("r", String(r - 2));
187
+ inner.setAttribute("fill", visual.colorHex);
188
+ inner.setAttribute("fill-opacity", "0.18");
189
+ inner.setAttribute("stroke", visual.colorHex);
190
+ inner.setAttribute("stroke-width", "1.4");
191
+ groupEl.appendChild(inner);
192
+ const codeEl = doc.createElementNS(SVG_NS, "text");
193
+ codeEl.setAttribute("x", "0");
194
+ codeEl.setAttribute("y", "-2");
195
+ codeEl.setAttribute("text-anchor", "middle");
196
+ codeEl.setAttribute("fill", "#f8fafc");
197
+ codeEl.setAttribute("font-size", "11");
198
+ codeEl.setAttribute("font-weight", "600");
199
+ codeEl.setAttribute("font-family", "IBM Plex Mono, monospace");
200
+ codeEl.textContent = visual.code;
201
+ groupEl.appendChild(codeEl);
202
+ const countEl = doc.createElementNS(SVG_NS, "text");
203
+ countEl.setAttribute("x", "0");
204
+ countEl.setAttribute("y", "11");
205
+ countEl.setAttribute("text-anchor", "middle");
206
+ countEl.setAttribute("fill", visual.colorHex);
207
+ countEl.setAttribute("font-size", "9");
208
+ countEl.setAttribute("font-family", "IBM Plex Mono, monospace");
209
+ countEl.textContent = `${visual.memberCount}n`;
210
+ groupEl.appendChild(countEl);
172
211
  }
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
- );
212
+ };
213
+ var MORPH_SAMPLES = 64;
214
+ function sampleShapePerimeter(shape, radius) {
215
+ const r = Math.max(3, radius);
216
+ const pts = [];
217
+ if (shape === "circle") {
218
+ for (let i = 0; i < MORPH_SAMPLES; i++) {
219
+ const a = i / MORPH_SAMPLES * Math.PI * 2 - Math.PI / 2;
220
+ pts.push({ x: Math.cos(a) * r, y: Math.sin(a) * r });
221
+ }
222
+ return pts;
183
223
  }
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)})`
224
+ const polyVerts = [];
225
+ if (shape === "diamond") {
226
+ const d = r * 1.12;
227
+ polyVerts.push(
228
+ { x: 0, y: -d },
229
+ { x: d * 0.88, y: 0 },
230
+ { x: 0, y: d },
231
+ { x: -d * 0.88, y: 0 }
192
232
  );
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();
233
+ } else if (shape === "hexagon") {
234
+ for (let i = 0; i < 6; i++) {
235
+ const a = -Math.PI / 2 + i * Math.PI * 2 / 6;
236
+ polyVerts.push({ x: Math.cos(a) * r * 1.05, y: Math.sin(a) * r * 1.05 });
201
237
  }
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));
238
+ } else {
239
+ const outer = r * 1.24;
240
+ const inner = r * 0.56;
241
+ for (let i = 0; i < 10; i++) {
242
+ const a = -Math.PI / 2 + i * Math.PI / 5;
243
+ const rad = i % 2 === 0 ? outer : inner;
244
+ polyVerts.push({ x: Math.cos(a) * rad, y: Math.sin(a) * rad });
211
245
  }
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);
246
+ }
247
+ const edgeLens = [];
248
+ let totalLen = 0;
249
+ for (let i = 0; i < polyVerts.length; i++) {
250
+ const a = polyVerts[i];
251
+ const b = polyVerts[(i + 1) % polyVerts.length];
252
+ const len = Math.hypot(b.x - a.x, b.y - a.y);
253
+ edgeLens.push(len);
254
+ totalLen += len;
255
+ }
256
+ for (let s = 0; s < MORPH_SAMPLES; s++) {
257
+ const targetDist = s / MORPH_SAMPLES * totalLen;
258
+ let accum = 0;
259
+ for (let i = 0; i < polyVerts.length; i++) {
260
+ const len = edgeLens[i];
261
+ if (accum + len >= targetDist || i === polyVerts.length - 1) {
262
+ const localT = len > 1e-6 ? (targetDist - accum) / len : 0;
263
+ const a = polyVerts[i];
264
+ const b = polyVerts[(i + 1) % polyVerts.length];
265
+ pts.push({
266
+ x: a.x + (b.x - a.x) * localT,
267
+ y: a.y + (b.y - a.y) * localT
268
+ });
269
+ break;
270
+ }
271
+ accum += len;
219
272
  }
220
- while (this.edgeLines.length > snapshot.edges.length) {
221
- const line = this.edgeLines.pop();
222
- line?.remove();
273
+ }
274
+ return pts;
275
+ }
276
+ function pointsToSvgPath(pts) {
277
+ if (pts.length === 0) return "";
278
+ let d = `M ${pts[0].x.toFixed(2)} ${pts[0].y.toFixed(2)}`;
279
+ for (let i = 1; i < pts.length; i++) {
280
+ const p = pts[i];
281
+ d += ` L ${p.x.toFixed(2)} ${p.y.toFixed(2)}`;
282
+ }
283
+ return `${d} Z`;
284
+ }
285
+ function parseHexRgb(hex) {
286
+ const clean = hex.startsWith("#") ? hex.slice(1) : hex;
287
+ if (clean.length < 6) return [148, 163, 184];
288
+ return [
289
+ parseInt(clean.slice(0, 2), 16) || 0,
290
+ parseInt(clean.slice(2, 4), 16) || 0,
291
+ parseInt(clean.slice(4, 6), 16) || 0
292
+ ];
293
+ }
294
+ function lerpHex(fromHex, toHex, t) {
295
+ const a = parseHexRgb(fromHex);
296
+ const b = parseHexRgb(toHex);
297
+ const r = Math.round(a[0] + (b[0] - a[0]) * t);
298
+ const g = Math.round(a[1] + (b[1] - a[1]) * t);
299
+ const bl = Math.round(a[2] + (b[2] - a[2]) * t);
300
+ return `rgb(${r}, ${g}, ${bl})`;
301
+ }
302
+ var GeometricShapeSvgView = class {
303
+ measureConvexHull(visual) {
304
+ const r = visual.radius + (visual.strokeWidth ?? 1.5);
305
+ if (visual.shape === "circle") {
306
+ return { kind: "circle", radius: r };
223
307
  }
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));
308
+ if (visual.shape === "diamond") {
309
+ const d = r * 1.12;
310
+ return {
311
+ kind: "polygon",
312
+ vertices: computeConvexHull([
313
+ { x: 0, y: -d },
314
+ { x: d * 0.88, y: 0 },
315
+ { x: 0, y: d },
316
+ { x: -d * 0.88, y: 0 }
317
+ ])
318
+ };
231
319
  }
232
- this.reconcileNodes(
233
- snapshot.clusters,
234
- this.clustersGroup,
235
- this.clusterDomMap,
236
- this.clusterView,
237
- this.clusterViewByKey,
238
- "cluster"
320
+ if (visual.shape === "hexagon") {
321
+ return regularPolygon(6, r * 1.05);
322
+ }
323
+ return starConvexHull(5, r * 1.24);
324
+ }
325
+ ensureElements(groupEl) {
326
+ let pathEl = groupEl.querySelector("path.sv-geom-shape");
327
+ let ringEl = groupEl.querySelector("path.sv-geom-ring");
328
+ if (!pathEl || !ringEl) {
329
+ while (groupEl.firstChild) groupEl.removeChild(groupEl.firstChild);
330
+ const doc = groupEl.ownerDocument;
331
+ ringEl = doc.createElementNS(SVG_NS, "path");
332
+ ringEl.setAttribute("class", "sv-geom-ring");
333
+ ringEl.setAttribute("fill", "none");
334
+ pathEl = doc.createElementNS(SVG_NS, "path");
335
+ pathEl.setAttribute("class", "sv-geom-shape");
336
+ groupEl.appendChild(ringEl);
337
+ groupEl.appendChild(pathEl);
338
+ }
339
+ return { pathEl, ringEl };
340
+ }
341
+ mountOrUpdate(groupEl, visual, state) {
342
+ const { pathEl, ringEl } = this.ensureElements(groupEl);
343
+ const pts = sampleShapePerimeter(visual.shape, visual.radius);
344
+ const d = pointsToSvgPath(pts);
345
+ pathEl.setAttribute("d", d);
346
+ pathEl.setAttribute("fill", visual.fillHex);
347
+ pathEl.setAttribute(
348
+ "stroke",
349
+ state.interaction.isPinned ? "#f59e0b" : state.interaction.isHovered ? "#f8fafc" : visual.strokeHex ?? "rgba(15, 23, 42, 0.8)"
239
350
  );
240
- this.reconcileNodes(
241
- snapshot.items,
242
- this.itemsGroup,
243
- this.itemDomMap,
244
- this.contentView,
245
- this.contentViewByKey,
246
- "item"
351
+ pathEl.setAttribute(
352
+ "stroke-width",
353
+ String(state.interaction.isHovered || state.interaction.isPinned ? 2.2 : visual.strokeWidth ?? 1.4)
247
354
  );
248
- this.renderHullOverlay(snapshot);
355
+ if (state.interaction.isHovered || state.interaction.isPinned || state.interaction.isHighlighted) {
356
+ const outerPts = sampleShapePerimeter(visual.shape, visual.radius + 4.5);
357
+ ringEl.setAttribute("d", pointsToSvgPath(outerPts));
358
+ ringEl.setAttribute("stroke", state.interaction.isPinned ? "#f59e0b" : "#22d3ee");
359
+ ringEl.setAttribute("stroke-width", "1.4");
360
+ ringEl.setAttribute("stroke-opacity", "0.9");
361
+ } else {
362
+ ringEl.setAttribute("d", "");
363
+ }
249
364
  }
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)})`
365
+ morph(groupEl, from, to, progress, state) {
366
+ const { pathEl, ringEl } = this.ensureElements(groupEl);
367
+ const ptsA = sampleShapePerimeter(from.shape, from.radius);
368
+ const ptsB = sampleShapePerimeter(to.shape, to.radius);
369
+ const morphed = [];
370
+ for (let i = 0; i < MORPH_SAMPLES; i++) {
371
+ const a = ptsA[i];
372
+ const b = ptsB[i];
373
+ morphed.push({
374
+ x: a.x + (b.x - a.x) * progress,
375
+ y: a.y + (b.y - a.y) * progress
376
+ });
377
+ }
378
+ pathEl.setAttribute("d", pointsToSvgPath(morphed));
379
+ pathEl.setAttribute("fill", lerpHex(from.fillHex, to.fillHex, progress));
380
+ pathEl.setAttribute(
381
+ "stroke",
382
+ state.interaction.isHovered ? "#f8fafc" : to.strokeHex ?? "rgba(15, 23, 42, 0.8)"
383
+ );
384
+ pathEl.setAttribute("stroke-width", String(to.strokeWidth ?? 1.4));
385
+ ringEl.setAttribute("d", "");
386
+ return true;
387
+ }
388
+ };
389
+
390
+ // src/views/MultiSectorGlyphSvgView.ts
391
+ function describeDonutSectorPath(innerR, outerR, startAngle, endAngle) {
392
+ const sweep = endAngle - startAngle;
393
+ if (sweep >= Math.PI * 2 - 1e-4) {
394
+ return [
395
+ `M ${outerR} 0`,
396
+ `A ${outerR} ${outerR} 0 1 0 ${-outerR} 0`,
397
+ `A ${outerR} ${outerR} 0 1 0 ${outerR} 0`,
398
+ `M ${innerR} 0`,
399
+ `A ${innerR} ${innerR} 0 1 1 ${-innerR} 0`,
400
+ `A ${innerR} ${innerR} 0 1 1 ${innerR} 0`,
401
+ "Z"
402
+ ].join(" ");
403
+ }
404
+ const largeArc = sweep > Math.PI ? 1 : 0;
405
+ const x1o = Math.cos(startAngle) * outerR;
406
+ const y1o = Math.sin(startAngle) * outerR;
407
+ const x2o = Math.cos(endAngle) * outerR;
408
+ const y2o = Math.sin(endAngle) * outerR;
409
+ const x1i = Math.cos(endAngle) * innerR;
410
+ const y1i = Math.sin(endAngle) * innerR;
411
+ const x2i = Math.cos(startAngle) * innerR;
412
+ const y2i = Math.sin(startAngle) * innerR;
413
+ return [
414
+ `M ${x1o.toFixed(2)} ${y1o.toFixed(2)}`,
415
+ `A ${outerR.toFixed(2)} ${outerR.toFixed(2)} 0 ${largeArc} 1 ${x2o.toFixed(2)} ${y2o.toFixed(2)}`,
416
+ `L ${x1i.toFixed(2)} ${y1i.toFixed(2)}`,
417
+ `A ${innerR.toFixed(2)} ${innerR.toFixed(2)} 0 ${largeArc} 0 ${x2i.toFixed(2)} ${y2i.toFixed(2)}`,
418
+ "Z"
419
+ ].join(" ");
420
+ }
421
+ var MultiSectorGlyphSvgView = class {
422
+ measureConvexHull(visual) {
423
+ return { kind: "circle", radius: visual.outerRadius + 2 };
424
+ }
425
+ mountOrUpdate(groupEl, visual, state) {
426
+ const doc = groupEl.ownerDocument;
427
+ while (groupEl.firstChild) {
428
+ groupEl.removeChild(groupEl.firstChild);
429
+ }
430
+ const totalWeight = visual.sectors.reduce((s, sec) => s + Math.max(0, sec.weight), 0) || 1;
431
+ let angle = -Math.PI / 2;
432
+ for (const sec of visual.sectors) {
433
+ const frac = Math.max(0, sec.weight) / totalWeight;
434
+ if (frac <= 5e-3) continue;
435
+ const nextAngle = angle + frac * Math.PI * 2;
436
+ const path = doc.createElementNS(SVG_NS, "path");
437
+ path.setAttribute(
438
+ "d",
439
+ describeDonutSectorPath(visual.innerRadius, visual.outerRadius, angle, nextAngle)
277
440
  );
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
- }
441
+ path.setAttribute("fill", sec.colorHex);
442
+ path.setAttribute("stroke", "#07090E");
443
+ path.setAttribute("stroke-width", visual.sectors.length > 1 ? "1.1" : "0.8");
444
+ groupEl.appendChild(path);
445
+ angle = nextAngle;
446
+ }
447
+ const core = doc.createElementNS(SVG_NS, "circle");
448
+ core.setAttribute("cx", "0");
449
+ core.setAttribute("cy", "0");
450
+ core.setAttribute("r", String(Math.max(2, visual.innerRadius - 1)));
451
+ core.setAttribute("fill", visual.centerColorHex);
452
+ groupEl.appendChild(core);
453
+ if (visual.accentRingHex || state.interaction.isHovered || state.interaction.isPinned || state.interaction.isHighlighted) {
454
+ const ring = doc.createElementNS(SVG_NS, "circle");
455
+ ring.setAttribute("cx", "0");
456
+ ring.setAttribute("cy", "0");
457
+ ring.setAttribute("r", String(visual.outerRadius + 3));
458
+ ring.setAttribute("fill", "none");
459
+ ring.setAttribute(
460
+ "stroke",
461
+ state.interaction.isPinned ? "#f59e0b" : state.interaction.isHovered ? "#f8fafc" : visual.accentRingHex ?? "#22d3ee"
462
+ );
463
+ ring.setAttribute("stroke-width", state.interaction.isHovered ? "2" : "1.2");
464
+ groupEl.appendChild(ring);
465
+ }
466
+ }
467
+ };
468
+
469
+ // src/presets.ts
470
+ function extractDefaultItemLabel(item, fallbackId, getLabel) {
471
+ if (getLabel) {
472
+ const custom = getLabel(item);
473
+ if (custom.length > 0) return custom;
474
+ }
475
+ if (typeof item === "object" && item !== null) {
476
+ if ("label" in item && typeof item.label === "string" && item.label.length > 0) {
477
+ return item.label;
478
+ }
479
+ if ("title" in item && typeof item.title === "string" && item.title.length > 0) {
480
+ return item.title;
481
+ }
482
+ if ("headline" in item && typeof item.headline === "string" && item.headline.length > 0) {
483
+ return item.headline;
484
+ }
485
+ if ("name" in item && typeof item.name === "string" && item.name.length > 0) {
486
+ return item.name;
487
+ }
488
+ }
489
+ return fallbackId;
490
+ }
491
+ function createPresetViews(preset) {
492
+ const contentView = preset === "glyphs" ? new MultiSectorGlyphSvgView() : new GeometricShapeSvgView();
493
+ const clusterView = preset === "badges" ? new BadgeClusterSvgView() : new ClusterAnchorSvgView();
494
+ return { contentView, clusterView };
495
+ }
496
+ function createPresetVisualResolver(preset, getLabel) {
497
+ return {
498
+ resolveContentVisual: (item, ctx) => {
499
+ if (preset === "glyphs") {
500
+ return {
501
+ outerRadius: 11.5,
502
+ innerRadius: 4.5,
503
+ sectors: ctx.unitClusters.map((uc) => ({
504
+ colorHex: uc.cluster.colorHex,
505
+ weight: uc.weight
506
+ })),
507
+ centerColorHex: ctx.mixedColorHex
508
+ };
509
+ }
510
+ const rank = ctx.unitClusters.length;
511
+ return {
512
+ shape: rank >= 3 ? "star" : rank === 2 ? "diamond" : "circle",
513
+ radius: 9.5,
514
+ fillHex: ctx.mixedColorHex,
515
+ strokeHex: "#07090E",
516
+ strokeWidth: 1.4,
517
+ label: extractDefaultItemLabel(item, ctx.id, getLabel)
518
+ };
519
+ },
520
+ resolveClusterVisual: (cluster) => {
521
+ if (preset === "badges") {
522
+ const count = cluster.members.length;
523
+ return {
524
+ code: cluster.code,
525
+ title: cluster.defaultLabel,
526
+ metaLine: `${count} ${count === 1 ? "item" : "items"}`,
527
+ colorHex: cluster.colorHex
528
+ };
529
+ }
530
+ return {
531
+ code: cluster.code,
532
+ label: cluster.defaultLabel,
533
+ memberCount: cluster.members.length,
534
+ colorHex: cluster.colorHex,
535
+ radius: 22
536
+ };
537
+ },
538
+ getVisualCacheKey: (visual) => {
539
+ if ("sectors" in visual) {
540
+ const secKey = visual.sectors.map((s) => `${s.colorHex}:${s.weight.toFixed(2)}`).join("|");
541
+ return `glyph:${visual.outerRadius}:${visual.centerColorHex}:${secKey}`;
542
+ }
543
+ if ("shape" in visual) {
544
+ return `geom:${visual.shape}:${visual.radius}:${visual.fillHex}`;
545
+ }
546
+ if ("metaLine" in visual) {
547
+ return `badge:${visual.code}:${visual.title}:${visual.metaLine}:${visual.colorHex}`;
548
+ }
549
+ return `anchor:${visual.code}:${visual.memberCount}:${visual.colorHex}`;
550
+ }
551
+ };
552
+ }
553
+ function interactionSignature(state) {
554
+ const { isHovered, isPinned, isDimmed, isHighlighted } = state.interaction;
555
+ return `${isHovered ? 1 : 0}${isPinned ? 1 : 0}${isDimmed ? 1 : 0}${isHighlighted ? 1 : 0}`;
556
+ }
557
+ var SvgRendererAdapter = class {
558
+ svgEl;
559
+ doc;
560
+ worldGroup;
561
+ trianglesGroup;
562
+ edgesGroup;
563
+ clustersGroup;
564
+ itemsGroup;
565
+ hullsGroup;
566
+ measureBin;
567
+ contentView;
568
+ clusterView;
569
+ contentHullCache = /* @__PURE__ */ new Map();
570
+ clusterHullCache = /* @__PURE__ */ new Map();
571
+ contentViewByKey = /* @__PURE__ */ new Map();
572
+ clusterViewByKey = /* @__PURE__ */ new Map();
573
+ clusterDomMap = /* @__PURE__ */ new Map();
574
+ itemDomMap = /* @__PURE__ */ new Map();
575
+ trianglePolygons = [];
576
+ edgeLines = [];
577
+ constructor(svgEl, options) {
578
+ this.svgEl = svgEl;
579
+ this.doc = svgEl.ownerDocument;
580
+ this.contentView = options.contentView;
581
+ this.clusterView = options.clusterView;
582
+ while (this.svgEl.firstChild) {
583
+ this.svgEl.removeChild(this.svgEl.firstChild);
584
+ }
585
+ this.worldGroup = this.createGroup("sv-world");
586
+ this.trianglesGroup = this.createGroup("sv-mesh-triangles");
587
+ this.edgesGroup = this.createGroup("sv-mesh-edges");
588
+ this.clustersGroup = this.createGroup("sv-clusters");
589
+ this.itemsGroup = this.createGroup("sv-items");
590
+ this.hullsGroup = this.createGroup("sv-hull-overlay");
591
+ this.hullsGroup.setAttribute("pointer-events", "none");
592
+ this.worldGroup.appendChild(this.trianglesGroup);
593
+ this.worldGroup.appendChild(this.edgesGroup);
594
+ this.worldGroup.appendChild(this.clustersGroup);
595
+ this.worldGroup.appendChild(this.itemsGroup);
596
+ this.worldGroup.appendChild(this.hullsGroup);
597
+ this.measureBin = this.createGroup("sv-measure-bin");
598
+ this.measureBin.setAttribute("visibility", "hidden");
599
+ this.measureBin.setAttribute("pointer-events", "none");
600
+ this.measureBin.setAttribute("transform", "translate(-99999, -99999)");
601
+ this.svgEl.appendChild(this.worldGroup);
602
+ this.svgEl.appendChild(this.measureBin);
603
+ }
604
+ createGroup(className) {
605
+ const g = this.doc.createElementNS(SVG_NS, "g");
606
+ g.setAttribute("class", className);
607
+ return g;
608
+ }
609
+ /**
610
+ * Swaps active content and/or cluster `SvgNodeView` plugins at runtime and invalidates
611
+ * hull caches so the engine re-measures and smoothly crossfades into the new views.
612
+ */
613
+ setViews(views) {
614
+ if (views.contentView && views.contentView !== this.contentView) {
615
+ this.contentView = views.contentView;
616
+ this.contentHullCache.clear();
617
+ }
618
+ if (views.clusterView && views.clusterView !== this.clusterView) {
619
+ this.clusterView = views.clusterView;
620
+ this.clusterHullCache.clear();
621
+ }
622
+ }
623
+ measureContentHull(visual, cacheKey) {
624
+ this.contentViewByKey.set(cacheKey, this.contentView);
625
+ return measureVisualWithSvgView(
626
+ this.contentView,
627
+ visual,
628
+ cacheKey,
629
+ this.svgEl,
630
+ this.measureBin,
631
+ this.contentHullCache
632
+ );
633
+ }
634
+ measureClusterHull(visual, cacheKey) {
635
+ this.clusterViewByKey.set(cacheKey, this.clusterView);
636
+ return measureVisualWithSvgView(
637
+ this.clusterView,
638
+ visual,
639
+ cacheKey,
640
+ this.svgEl,
641
+ this.measureBin,
642
+ this.clusterHullCache
643
+ );
644
+ }
645
+ renderFrame(snapshot) {
646
+ this.svgEl.setAttribute(
647
+ "viewBox",
648
+ `0 0 ${snapshot.viewportWidth} ${snapshot.viewportHeight}`
649
+ );
650
+ this.worldGroup.setAttribute(
651
+ "transform",
652
+ `translate(${snapshot.camera.panX.toFixed(2)}, ${snapshot.camera.panY.toFixed(2)}) scale(${snapshot.camera.zoom.toFixed(4)})`
653
+ );
654
+ while (this.trianglePolygons.length < snapshot.triangles.length) {
655
+ const poly = this.doc.createElementNS(SVG_NS, "polygon");
656
+ this.trianglesGroup.appendChild(poly);
657
+ this.trianglePolygons.push(poly);
658
+ }
659
+ while (this.trianglePolygons.length > snapshot.triangles.length) {
660
+ const poly = this.trianglePolygons.pop();
661
+ poly?.remove();
662
+ }
663
+ for (let i = 0; i < snapshot.triangles.length; i++) {
664
+ const tr = snapshot.triangles[i];
665
+ const poly = this.trianglePolygons[i];
666
+ poly.setAttribute(
667
+ "points",
668
+ tr.points.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ")
669
+ );
670
+ poly.setAttribute("fill", tr.fillHex);
671
+ poly.setAttribute("fill-opacity", String(tr.fillOpacity));
672
+ }
673
+ while (this.edgeLines.length < snapshot.edges.length) {
674
+ const line = this.doc.createElementNS(SVG_NS, "line");
675
+ line.setAttribute("stroke", "rgba(148, 163, 184, 0.16)");
676
+ line.setAttribute("stroke-width", "1");
677
+ line.setAttribute("stroke-dasharray", "3 4");
678
+ this.edgesGroup.appendChild(line);
679
+ this.edgeLines.push(line);
680
+ }
681
+ while (this.edgeLines.length > snapshot.edges.length) {
682
+ const line = this.edgeLines.pop();
683
+ line?.remove();
684
+ }
685
+ for (let i = 0; i < snapshot.edges.length; i++) {
686
+ const edge = snapshot.edges[i];
687
+ const line = this.edgeLines[i];
688
+ line.setAttribute("x1", edge.a.x.toFixed(1));
689
+ line.setAttribute("y1", edge.a.y.toFixed(1));
690
+ line.setAttribute("x2", edge.b.x.toFixed(1));
691
+ line.setAttribute("y2", edge.b.y.toFixed(1));
692
+ }
693
+ this.reconcileNodes(
694
+ snapshot.clusters,
695
+ this.clustersGroup,
696
+ this.clusterDomMap,
697
+ this.clusterView,
698
+ this.clusterViewByKey,
699
+ "cluster"
700
+ );
701
+ this.reconcileNodes(
702
+ snapshot.items,
703
+ this.itemsGroup,
704
+ this.itemDomMap,
705
+ this.contentView,
706
+ this.contentViewByKey,
707
+ "item"
708
+ );
709
+ this.renderHullOverlay(snapshot);
710
+ }
711
+ reconcileNodes(nodes, parentGroup, domMap, activeView, viewByKey, kind) {
712
+ const aliveIds = /* @__PURE__ */ new Set();
713
+ for (const node of nodes) {
714
+ aliveIds.add(node.id);
715
+ let tracked = domMap.get(node.id);
716
+ if (!tracked) {
717
+ const rootGroup = this.doc.createElementNS(SVG_NS, "g");
718
+ rootGroup.setAttribute("class", `sv-node sv-node-${kind}`);
719
+ rootGroup.setAttribute("data-node-id", node.id);
720
+ parentGroup.appendChild(rootGroup);
721
+ tracked = {
722
+ rootGroup,
723
+ mainLayer: null,
724
+ fromLayer: null,
725
+ toLayer: null,
726
+ lastMainKey: null,
727
+ lastFromKey: null,
728
+ lastToKey: null,
729
+ lastInteractionSig: null,
730
+ lastMorphUsed: false
731
+ };
732
+ domMap.set(node.id, tracked);
733
+ }
734
+ const effectiveOpacity = node.interaction.isDimmed ? node.opacity * 0.24 : node.opacity;
735
+ tracked.rootGroup.setAttribute(
736
+ "transform",
737
+ `translate(${node.x.toFixed(2)}, ${node.y.toFixed(2)}) scale(${node.scale.toFixed(4)})`
738
+ );
739
+ tracked.rootGroup.setAttribute("opacity", effectiveOpacity.toFixed(3));
740
+ const interSig = interactionSignature(node);
741
+ const interactionChanged = tracked.lastInteractionSig !== interSig;
742
+ if (node.transition) {
743
+ const tr = node.transition;
744
+ const fromView = viewByKey.get(tr.fromCacheKey) ?? tracked.lastView ?? activeView;
745
+ const toView = viewByKey.get(tr.toCacheKey) ?? activeView;
746
+ if (fromView === toView && toView.morph) {
747
+ if (!tracked.mainLayer) {
748
+ tracked.mainLayer = this.createGroup("sv-layer-main");
749
+ tracked.rootGroup.appendChild(tracked.mainLayer);
750
+ }
290
751
  if (tracked.fromLayer) {
291
752
  tracked.fromLayer.remove();
292
753
  tracked.fromLayer = null;
@@ -378,744 +839,563 @@ var SvgRendererAdapter = class {
378
839
  viewToUse.mountOrUpdate(tracked.mainLayer, node.visual, node);
379
840
  tracked.lastMainKey = node.cacheKey;
380
841
  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();
842
+ tracked.lastMorphUsed = false;
843
+ tracked.lastView = viewToUse;
844
+ }
845
+ }
664
846
  }
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();
847
+ for (const [id, tracked] of domMap) {
848
+ if (!aliveIds.has(id)) {
849
+ tracked.rootGroup.remove();
850
+ domMap.delete(id);
851
+ }
680
852
  }
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
853
  }
700
- ensureLoopRunning() {
701
- if (this.destroyed || this.rafId !== null) return;
702
- if (typeof requestAnimationFrame === "undefined") {
703
- this.engine.renderNow();
704
- return;
854
+ renderHullOverlay(snapshot) {
855
+ while (this.hullsGroup.firstChild) {
856
+ this.hullsGroup.removeChild(this.hullsGroup.firstChild);
705
857
  }
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;
858
+ if (!snapshot.showHulls) return;
859
+ const drawHull = (node, strokeColor) => {
860
+ if (node.opacity < 0.05) return;
861
+ const hull = node.hull;
862
+ if (hull.kind === "circle") {
863
+ const circle = this.doc.createElementNS(SVG_NS, "circle");
864
+ circle.setAttribute("cx", node.x.toFixed(2));
865
+ circle.setAttribute("cy", node.y.toFixed(2));
866
+ circle.setAttribute("r", (hull.radius * node.scale).toFixed(2));
867
+ circle.setAttribute("fill", strokeColor);
868
+ circle.setAttribute("fill-opacity", "0.07");
869
+ circle.setAttribute("stroke", strokeColor);
870
+ circle.setAttribute("stroke-width", "1");
871
+ circle.setAttribute("stroke-dasharray", "3 2");
872
+ this.hullsGroup.appendChild(circle);
873
+ } else if (hull.vertices.length >= 3) {
874
+ const poly = this.doc.createElementNS(SVG_NS, "polygon");
875
+ poly.setAttribute(
876
+ "points",
877
+ hull.vertices.map(
878
+ (v) => `${(node.x + v.x * node.scale).toFixed(2)},${(node.y + v.y * node.scale).toFixed(2)}`
879
+ ).join(" ")
880
+ );
881
+ poly.setAttribute("fill", strokeColor);
882
+ poly.setAttribute("fill-opacity", "0.07");
883
+ poly.setAttribute("stroke", strokeColor);
884
+ poly.setAttribute("stroke-width", "1");
885
+ poly.setAttribute("stroke-dasharray", "3 2");
886
+ this.hullsGroup.appendChild(poly);
724
887
  }
725
888
  };
726
- this.rafId = requestAnimationFrame(tick);
889
+ for (const c of snapshot.clusters) {
890
+ drawHull(c, "#f59e0b");
891
+ }
892
+ for (const it of snapshot.items) {
893
+ drawHull(it, "#22d3ee");
894
+ }
727
895
  }
728
896
  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);
897
+ this.clusterDomMap.clear();
898
+ this.itemDomMap.clear();
899
+ this.contentHullCache.clear();
900
+ this.clusterHullCache.clear();
901
+ this.contentViewByKey.clear();
902
+ this.clusterViewByKey.clear();
903
+ while (this.svgEl.firstChild) {
904
+ this.svgEl.removeChild(this.svgEl.firstChild);
740
905
  }
741
906
  }
742
907
  };
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;
908
+ function resolveContainerElement(target) {
909
+ if (typeof target !== "string") {
910
+ return target;
753
911
  }
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 }
912
+ if (typeof document === "undefined") {
913
+ throw new Error(
914
+ `SimplexSvgController: cannot resolve selector "${target}" outside a DOM environment.`
762
915
  );
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
916
  }
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;
917
+ const el = document.querySelector(target);
918
+ if (!el) {
919
+ throw new Error(
920
+ `SimplexSvgController: no element matched container selector "${target}".`
921
+ );
785
922
  }
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;
923
+ return el;
924
+ }
925
+ var SimplexSvgController = class _SimplexSvgController {
926
+ containerEl;
927
+ svgEl;
928
+ adapter;
929
+ engine;
930
+ resizeObserver = null;
931
+ onFpsUpdate;
932
+ focusOnClusterClick;
933
+ getIdExtractor;
934
+ getLabelExtractor;
935
+ tooltipOption;
936
+ tooltipEl = null;
937
+ focusedClusterUidState = null;
938
+ rafId = null;
939
+ lastFrameTs = 0;
940
+ fpsSamples = 0;
941
+ fpsAccumMs = 0;
942
+ destroyed = false;
943
+ // Centralized drag-to-pan state on root <svg>
944
+ isDraggingPan = false;
945
+ dragStartX = 0;
946
+ dragStartY = 0;
947
+ dragMoved = false;
948
+ constructor(containerEl, options) {
949
+ this.containerEl = resolveContainerElement(containerEl);
950
+ this.onFpsUpdate = options.onFpsUpdate;
951
+ this.focusOnClusterClick = options.focusOnClusterClick ?? true;
952
+ this.getLabelExtractor = options.getLabel;
953
+ this.tooltipOption = options.tooltip ?? false;
954
+ this.getIdExtractor = options.getId ?? ((item) => typeof item === "object" && item !== null && "id" in item ? String(item.id) : "");
955
+ const activePreset = options.preset ?? "geometric";
956
+ const presetViews = createPresetViews(activePreset);
957
+ const contentView = options.contentView ?? presetViews.contentView;
958
+ const clusterView = options.clusterView ?? presetViews.clusterView;
959
+ const visualResolver = options.visualResolver ?? createPresetVisualResolver(
960
+ activePreset,
961
+ this.getLabelExtractor
962
+ );
963
+ const doc = this.containerEl.ownerDocument;
964
+ this.svgEl = doc.createElementNS(SVG_NS, "svg");
965
+ this.svgEl.setAttribute("width", "100%");
966
+ this.svgEl.setAttribute("height", "100%");
967
+ this.svgEl.style.display = "block";
968
+ this.svgEl.style.touchAction = "none";
969
+ this.svgEl.style.userSelect = "none";
970
+ this.containerEl.appendChild(this.svgEl);
971
+ const rect = this.containerEl.getBoundingClientRect();
972
+ const initialW = rect.width > 40 ? rect.width : 960;
973
+ const initialH = rect.height > 40 ? rect.height : 620;
974
+ this.adapter = new SvgRendererAdapter(this.svgEl, {
975
+ contentView,
976
+ clusterView
977
+ });
978
+ this.engine = SimplexGraphEngine.createFromResolved({
979
+ items: options.items,
980
+ getId: options.getId,
981
+ getTags: options.getTags,
982
+ deriveClusterMeta: options.deriveClusterMeta,
983
+ visualResolver,
984
+ renderer: this.adapter,
985
+ viewportWidth: initialW,
986
+ viewportHeight: initialH,
987
+ normalizeTag: options.normalizeTag,
988
+ resolveTagMeta: options.resolveTagMeta,
989
+ meshOptions: options.meshOptions,
990
+ layoutOptions: options.layoutOptions,
991
+ showHulls: options.showHulls,
992
+ interactions: this.wrapInteractions(options.interactions),
993
+ onMetricsChange: options.onMetricsChange,
994
+ onClustersChange: options.onClustersChange
995
+ });
996
+ this.attachSvgListeners();
997
+ if (typeof ResizeObserver !== "undefined") {
998
+ this.resizeObserver = new ResizeObserver((entries) => {
999
+ if (this.destroyed) return;
1000
+ for (const entry of entries) {
1001
+ const { width, height } = entry.contentRect;
1002
+ if (width > 60 && height > 60) {
1003
+ this.engine.setViewport(width, height);
1004
+ this.ensureLoopRunning();
1005
+ }
1006
+ }
1007
+ });
1008
+ this.resizeObserver.observe(this.containerEl);
802
1009
  }
1010
+ this.ensureLoopRunning();
803
1011
  }
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)}`;
1012
+ static createFromResolved(containerEl, options) {
1013
+ return new _SimplexSvgController(containerEl, options);
1014
+ }
1015
+ wrapInteractions(userInteractions) {
1016
+ return {
1017
+ onContentHover: (ev) => {
1018
+ userInteractions?.onContentHover?.(ev);
1019
+ if (ev) {
1020
+ this.showTooltip(
1021
+ ev.screenPosition.x,
1022
+ ev.screenPosition.y,
1023
+ this.formatContentTooltip(ev)
1024
+ );
1025
+ } else {
1026
+ this.hideTooltip();
1027
+ }
1028
+ },
1029
+ onClusterHover: (ev) => {
1030
+ userInteractions?.onClusterHover?.(ev);
1031
+ if (ev) {
1032
+ this.showTooltip(
1033
+ ev.screenPosition.x,
1034
+ ev.screenPosition.y,
1035
+ this.formatClusterTooltip(ev)
1036
+ );
1037
+ } else {
1038
+ this.hideTooltip();
1039
+ }
1040
+ },
1041
+ onLeftClick: (target, rawEvent) => {
1042
+ if (userInteractions?.onLeftClick) {
1043
+ userInteractions.onLeftClick(target, rawEvent);
1044
+ return;
1045
+ }
1046
+ if (!this.focusOnClusterClick) return;
1047
+ if (target.kind === "cluster") {
1048
+ const next = this.focusedClusterUidState === target.cluster.clusterUid ? null : target.cluster.clusterUid;
1049
+ this.setFocusedClusterUid(next);
1050
+ } else if (target.kind === "background" && this.focusedClusterUidState !== null) {
1051
+ this.setFocusedClusterUid(null);
1052
+ }
1053
+ },
1054
+ onRightClick: userInteractions?.onRightClick,
1055
+ onWheel: userInteractions?.onWheel ?? "zoom-to-cursor"
1056
+ };
812
1057
  }
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 };
1058
+ formatContentTooltip(ev) {
1059
+ if (typeof this.tooltipOption === "function") {
1060
+ return this.tooltipOption({ kind: "content", ...ev });
837
1061
  }
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
- };
1062
+ const label = extractDefaultItemLabel(
1063
+ ev.item,
1064
+ ev.layoutContext.id,
1065
+ this.getLabelExtractor
1066
+ );
1067
+ const tagList = ev.layoutContext.tags.map((t) => `#${t.tag}`).slice(0, 5).join(" \xB7 ");
1068
+ return tagList ? `${label}
1069
+ ${tagList}` : label;
1070
+ }
1071
+ formatClusterTooltip(ev) {
1072
+ if (typeof this.tooltipOption === "function") {
1073
+ return this.tooltipOption({ kind: "cluster", ...ev });
849
1074
  }
850
- if (visual.shape === "hexagon") {
851
- return regularPolygon(6, r * 1.05);
1075
+ const count = ev.cluster.members.length;
1076
+ return `${ev.cluster.code} \xB7 ${ev.cluster.defaultLabel}
1077
+ ${count} ${count === 1 ? "item" : "items"}`;
1078
+ }
1079
+ ensureTooltipElement() {
1080
+ if (!this.tooltipOption) return null;
1081
+ if (this.tooltipEl) return this.tooltipEl;
1082
+ const doc = this.containerEl.ownerDocument;
1083
+ const computedPos = typeof getComputedStyle !== "undefined" ? getComputedStyle(this.containerEl).position : this.containerEl.style.position;
1084
+ if (!computedPos || computedPos === "static") {
1085
+ this.containerEl.style.position = "relative";
852
1086
  }
853
- return starConvexHull(5, r * 1.24);
1087
+ const el = doc.createElement("div");
1088
+ el.className = "sv-tooltip";
1089
+ el.style.position = "absolute";
1090
+ el.style.pointerEvents = "none";
1091
+ el.style.zIndex = "20";
1092
+ el.style.display = "none";
1093
+ el.style.maxWidth = "240px";
1094
+ el.style.padding = "7px 10px";
1095
+ el.style.borderRadius = "8px";
1096
+ el.style.background = "rgba(9, 13, 22, 0.94)";
1097
+ el.style.border = "1px solid rgba(51, 65, 85, 0.85)";
1098
+ el.style.color = "#f1f5f9";
1099
+ el.style.fontFamily = "Plus Jakarta Sans, ui-sans-serif, system-ui, sans-serif";
1100
+ el.style.fontSize = "12px";
1101
+ el.style.lineHeight = "1.4";
1102
+ el.style.whiteSpace = "pre-line";
1103
+ el.style.boxShadow = "0 10px 24px rgba(0, 0, 0, 0.45)";
1104
+ this.containerEl.appendChild(el);
1105
+ this.tooltipEl = el;
1106
+ return el;
854
1107
  }
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);
1108
+ showTooltip(screenX, screenY, text) {
1109
+ const el = this.ensureTooltipElement();
1110
+ if (!el || !text) return;
1111
+ el.textContent = text;
1112
+ el.style.display = "block";
1113
+ const maxLeft = Math.max(8, (this.containerEl.clientWidth || 960) - 256);
1114
+ const maxTop = Math.max(8, (this.containerEl.clientHeight || 600) - 72);
1115
+ el.style.left = `${Math.min(Math.max(8, Math.round(screenX + 14)), maxLeft)}px`;
1116
+ el.style.top = `${Math.min(Math.max(8, Math.round(screenY + 14)), maxTop)}px`;
1117
+ }
1118
+ hideTooltip() {
1119
+ if (this.tooltipEl) {
1120
+ this.tooltipEl.style.display = "none";
868
1121
  }
869
- return { pathEl, ringEl };
870
1122
  }
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)"
1123
+ get camera() {
1124
+ return this.engine.camera;
1125
+ }
1126
+ getSvgElement() {
1127
+ return this.svgEl;
1128
+ }
1129
+ getMetrics() {
1130
+ return this.engine.getMetrics();
1131
+ }
1132
+ getClusters() {
1133
+ return this.engine.getClusters();
1134
+ }
1135
+ getItems() {
1136
+ return this.engine.getItems();
1137
+ }
1138
+ addItems(items) {
1139
+ this.engine.addItems(items);
1140
+ this.ensureLoopRunning();
1141
+ }
1142
+ removeItems(ids) {
1143
+ this.engine.removeItems(ids);
1144
+ this.ensureLoopRunning();
1145
+ }
1146
+ setItems(items) {
1147
+ this.engine.setItems(items);
1148
+ this.ensureLoopRunning();
1149
+ }
1150
+ /**
1151
+ * Switches between built-in presets (`'geometric'`, `'glyphs'`, `'badges'`) with smooth
1152
+ * morph/crossfade transitions.
1153
+ */
1154
+ setPreset(preset, getLabel) {
1155
+ if (getLabel !== void 0) {
1156
+ this.getLabelExtractor = getLabel;
1157
+ }
1158
+ const views = createPresetViews(preset);
1159
+ const resolver = createPresetVisualResolver(
1160
+ preset,
1161
+ this.getLabelExtractor
880
1162
  );
881
- pathEl.setAttribute(
882
- "stroke-width",
883
- String(state.interaction.isHovered || state.interaction.isPinned ? 2.2 : visual.strokeWidth ?? 1.4)
1163
+ this.adapter.setViews({
1164
+ contentView: views.contentView,
1165
+ clusterView: views.clusterView
1166
+ });
1167
+ this.engine.setVisualResolver(
1168
+ resolver
884
1169
  );
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
- }
1170
+ this.ensureLoopRunning();
894
1171
  }
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
- });
1172
+ setTooltip(tooltip) {
1173
+ this.tooltipOption = tooltip;
1174
+ if (!tooltip) {
1175
+ this.hideTooltip();
907
1176
  }
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
1177
  }
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(" ");
1178
+ setVisualResolver(resolver) {
1179
+ this.engine.setVisualResolver(resolver);
1180
+ this.ensureLoopRunning();
933
1181
  }
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 };
1182
+ setViews(views) {
1183
+ this.adapter.setViews(views);
954
1184
  }
955
- mountOrUpdate(groupEl, visual, state) {
956
- const doc = groupEl.ownerDocument;
957
- while (groupEl.firstChild) {
958
- groupEl.removeChild(groupEl.firstChild);
1185
+ setSizeMultiplier(multiplier) {
1186
+ this.engine.setSizeMultiplier(multiplier);
1187
+ this.ensureLoopRunning();
1188
+ }
1189
+ setShowHulls(show) {
1190
+ this.engine.setShowHulls(show);
1191
+ this.ensureLoopRunning();
1192
+ }
1193
+ setInteractions(interactions) {
1194
+ this.engine.setInteractions(this.wrapInteractions(interactions));
1195
+ }
1196
+ setPinnedIds(ids) {
1197
+ this.engine.setPinnedIds(ids);
1198
+ this.ensureLoopRunning();
1199
+ }
1200
+ setHighlightedIds(ids) {
1201
+ this.engine.setHighlightedIds(ids);
1202
+ this.ensureLoopRunning();
1203
+ }
1204
+ setFocusedClusterUid(clusterUid) {
1205
+ this.focusedClusterUidState = clusterUid;
1206
+ this.engine.setFocusedClusterUid(clusterUid);
1207
+ this.ensureLoopRunning();
1208
+ }
1209
+ /** Deterministically advances simulation time in tests without waiting for rAF. */
1210
+ stepForTest(dtMs) {
1211
+ return this.engine.step(dtMs);
1212
+ }
1213
+ toLocalCoords(clientX, clientY) {
1214
+ const rect = this.svgEl.getBoundingClientRect();
1215
+ return {
1216
+ x: clientX - rect.left,
1217
+ y: clientY - rect.top
1218
+ };
1219
+ }
1220
+ onPointerDown = (e) => {
1221
+ if (e.button !== 0) return;
1222
+ const { x, y } = this.toLocalCoords(e.clientX, e.clientY);
1223
+ const hit = this.engine.hitTest(x, y);
1224
+ if (hit.kind === "background") {
1225
+ this.isDraggingPan = true;
1226
+ this.dragStartX = e.clientX;
1227
+ this.dragStartY = e.clientY;
1228
+ this.dragMoved = false;
1229
+ this.svgEl.style.cursor = "grabbing";
1230
+ } else {
1231
+ this.isDraggingPan = false;
1232
+ this.dragMoved = false;
1233
+ }
1234
+ };
1235
+ onPointerMove = (e) => {
1236
+ const { x, y } = this.toLocalCoords(e.clientX, e.clientY);
1237
+ if (this.isDraggingPan) {
1238
+ const dx = e.clientX - this.dragStartX;
1239
+ const dy = e.clientY - this.dragStartY;
1240
+ if (Math.hypot(dx, dy) > 3) {
1241
+ this.dragMoved = true;
1242
+ }
1243
+ if (this.dragMoved) {
1244
+ this.dragStartX = e.clientX;
1245
+ this.dragStartY = e.clientY;
1246
+ this.engine.camera.panBy(dx, dy);
1247
+ this.hideTooltip();
1248
+ this.ensureLoopRunning();
1249
+ }
1250
+ return;
1251
+ }
1252
+ this.engine.handlePointerMove(x, y);
1253
+ const hit = this.engine.hitTest(x, y);
1254
+ this.svgEl.style.cursor = hit.kind === "background" ? "grab" : "pointer";
1255
+ this.ensureLoopRunning();
1256
+ };
1257
+ onPointerUp = () => {
1258
+ if (this.isDraggingPan) {
1259
+ this.isDraggingPan = false;
1260
+ this.svgEl.style.cursor = "grab";
1261
+ }
1262
+ };
1263
+ onPointerLeave = () => {
1264
+ this.isDraggingPan = false;
1265
+ this.hideTooltip();
1266
+ this.engine.handlePointerLeave();
1267
+ this.ensureLoopRunning();
1268
+ };
1269
+ onClick = (e) => {
1270
+ if (this.dragMoved) {
1271
+ this.dragMoved = false;
1272
+ return;
959
1273
  }
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;
1274
+ const { x, y } = this.toLocalCoords(e.clientX, e.clientY);
1275
+ this.engine.handleLeftClick({
1276
+ screenX: x,
1277
+ screenY: y,
1278
+ button: e.button,
1279
+ shiftKey: e.shiftKey,
1280
+ ctrlKey: e.ctrlKey,
1281
+ altKey: e.altKey,
1282
+ metaKey: e.metaKey
1283
+ });
1284
+ this.ensureLoopRunning();
1285
+ };
1286
+ onContextMenu = (e) => {
1287
+ const { x, y } = this.toLocalCoords(e.clientX, e.clientY);
1288
+ const suppress = this.engine.handleRightClick({
1289
+ screenX: x,
1290
+ screenY: y,
1291
+ button: e.button,
1292
+ shiftKey: e.shiftKey,
1293
+ ctrlKey: e.ctrlKey,
1294
+ altKey: e.altKey,
1295
+ metaKey: e.metaKey
1296
+ });
1297
+ if (suppress) {
1298
+ e.preventDefault();
976
1299
  }
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);
1300
+ this.ensureLoopRunning();
1301
+ };
1302
+ onWheel = (e) => {
1303
+ const { x, y } = this.toLocalCoords(e.clientX, e.clientY);
1304
+ const handled = this.engine.handleWheel({
1305
+ screenX: x,
1306
+ screenY: y,
1307
+ deltaX: e.deltaX,
1308
+ deltaY: e.deltaY,
1309
+ ctrlKey: e.ctrlKey,
1310
+ shiftKey: e.shiftKey
1311
+ });
1312
+ if (handled) {
1313
+ e.preventDefault();
1314
+ this.ensureLoopRunning();
995
1315
  }
1316
+ };
1317
+ attachSvgListeners() {
1318
+ this.svgEl.addEventListener("pointerdown", this.onPointerDown);
1319
+ this.svgEl.addEventListener("pointermove", this.onPointerMove);
1320
+ this.svgEl.addEventListener("pointerup", this.onPointerUp);
1321
+ this.svgEl.addEventListener("pointerleave", this.onPointerLeave);
1322
+ this.svgEl.addEventListener("click", this.onClick);
1323
+ this.svgEl.addEventListener("contextmenu", this.onContextMenu);
1324
+ this.svgEl.addEventListener("wheel", this.onWheel, { passive: false });
996
1325
  }
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);
1326
+ detachSvgListeners() {
1327
+ this.svgEl.removeEventListener("pointerdown", this.onPointerDown);
1328
+ this.svgEl.removeEventListener("pointermove", this.onPointerMove);
1329
+ this.svgEl.removeEventListener("pointerup", this.onPointerUp);
1330
+ this.svgEl.removeEventListener("pointerleave", this.onPointerLeave);
1331
+ this.svgEl.removeEventListener("click", this.onClick);
1332
+ this.svgEl.removeEventListener("contextmenu", this.onContextMenu);
1333
+ this.svgEl.removeEventListener("wheel", this.onWheel);
1334
+ }
1335
+ ensureLoopRunning() {
1336
+ if (this.destroyed || this.rafId !== null) return;
1337
+ if (typeof requestAnimationFrame === "undefined") {
1338
+ this.engine.renderNow();
1339
+ return;
1005
1340
  }
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);
1341
+ this.lastFrameTs = performance.now();
1342
+ const tick = (now) => {
1343
+ if (this.destroyed) return;
1344
+ const dtMs = Math.max(1, Math.min(50, now - this.lastFrameTs));
1345
+ this.lastFrameTs = now;
1346
+ this.fpsSamples += 1;
1347
+ this.fpsAccumMs += dtMs;
1348
+ if (this.fpsAccumMs >= 320) {
1349
+ const fps = Math.round(this.fpsSamples * 1e3 / this.fpsAccumMs);
1350
+ this.onFpsUpdate?.(fps);
1351
+ this.fpsSamples = 0;
1352
+ this.fpsAccumMs = 0;
1353
+ }
1354
+ const settled = this.engine.step(dtMs);
1355
+ if (!settled) {
1356
+ this.rafId = requestAnimationFrame(tick);
1357
+ } else {
1358
+ this.rafId = null;
1359
+ }
1360
+ };
1361
+ this.rafId = requestAnimationFrame(tick);
1061
1362
  }
1062
- };
1063
-
1064
- // src/views/ClusterAnchorSvgView.ts
1065
- var ClusterAnchorSvgView = class {
1066
- measureConvexHull(visual) {
1067
- return { kind: "circle", radius: visual.radius + 5 };
1363
+ /** Helper accessor matching `getId` when external callers inspect items. */
1364
+ getItemId(item) {
1365
+ return this.getIdExtractor(item);
1068
1366
  }
1069
- mountOrUpdate(groupEl, visual, state) {
1070
- const doc = groupEl.ownerDocument;
1071
- while (groupEl.firstChild) {
1072
- groupEl.removeChild(groupEl.firstChild);
1367
+ destroy() {
1368
+ if (this.destroyed) return;
1369
+ this.destroyed = true;
1370
+ if (this.rafId !== null && typeof cancelAnimationFrame !== "undefined") {
1371
+ cancelAnimationFrame(this.rafId);
1372
+ this.rafId = null;
1373
+ }
1374
+ this.resizeObserver?.disconnect();
1375
+ this.detachSvgListeners();
1376
+ this.engine.destroy();
1377
+ if (this.tooltipEl && this.tooltipEl.parentElement === this.containerEl) {
1378
+ this.containerEl.removeChild(this.tooltipEl);
1379
+ this.tooltipEl = null;
1380
+ }
1381
+ if (this.svgEl.parentElement === this.containerEl) {
1382
+ this.containerEl.removeChild(this.svgEl);
1073
1383
  }
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
1384
  }
1117
1385
  };
1386
+ function createSimplexSvg(target, itemsOrOptions) {
1387
+ if (Array.isArray(itemsOrOptions)) {
1388
+ return SimplexSvgController.createFromResolved(target, {
1389
+ items: itemsOrOptions,
1390
+ tooltip: true
1391
+ });
1392
+ }
1393
+ return new SimplexSvgController(
1394
+ target,
1395
+ itemsOrOptions
1396
+ );
1397
+ }
1118
1398
 
1119
- export { BadgeClusterSvgView, ClusterAnchorSvgView, GeometricShapeSvgView, MultiSectorGlyphSvgView, SVG_NS, SimplexSvgController, SvgRendererAdapter, measureVisualWithSvgView };
1399
+ export { BadgeClusterSvgView, ClusterAnchorSvgView, GeometricShapeSvgView, MultiSectorGlyphSvgView, SVG_NS, SimplexSvgController, SvgRendererAdapter, createPresetViews, createPresetVisualResolver, createSimplexSvg, extractDefaultItemLabel, measureVisualWithSvgView };
1120
1400
  //# sourceMappingURL=index.js.map
1121
1401
  //# sourceMappingURL=index.js.map