@omiron33/omi-neuron-web 0.2.21 → 0.2.23

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.
@@ -32,11 +32,26 @@ function _interopNamespace(e) {
32
32
  var THREE3__namespace = /*#__PURE__*/_interopNamespace(THREE3);
33
33
 
34
34
  // src/visualization/constants.ts
35
+ var DEFAULT_STATUS_COLORS = {
36
+ default: "#c0c5ff",
37
+ // Same as defaultDomainColor (lavender)
38
+ draft: "#9ca3af",
39
+ // Gray
40
+ active: "#4ade80",
41
+ // Green
42
+ complete: "#60a5fa",
43
+ // Blue
44
+ blocked: "#f87171",
45
+ // Red
46
+ archived: "#6b7280"
47
+ // Dark gray
48
+ };
35
49
  var DEFAULT_THEME = {
36
50
  colors: {
37
51
  background: "#020314",
38
52
  domainColors: {},
39
53
  defaultDomainColor: "#c0c5ff",
54
+ statusColors: DEFAULT_STATUS_COLORS,
40
55
  edgeDefault: "#4d4d55",
41
56
  edgeActive: "#c6d4ff",
42
57
  edgeSelected: "#ffffff",
@@ -505,9 +520,14 @@ var NodeRenderer = class {
505
520
  const shouldRenderLabels = labelVisibility !== "none" && (labelVisibility === "interaction" || this.config.maxVisibleLabels > 0 && this.config.labelDistance > 0);
506
521
  const resolveNode = (node) => {
507
522
  const tier = node.tier ?? "tertiary";
508
- const baseColor = new THREE3__namespace.Color(
509
- this.config.domainColors[node.domain] ?? this.config.defaultColor
510
- );
523
+ let baseColor;
524
+ if (node.status && this.config.statusColors?.[node.status]) {
525
+ baseColor = new THREE3__namespace.Color(this.config.statusColors[node.status]);
526
+ } else {
527
+ baseColor = new THREE3__namespace.Color(
528
+ this.config.domainColors[node.domain] ?? this.config.defaultColor
529
+ );
530
+ }
511
531
  const position = new THREE3__namespace.Vector3();
512
532
  if (node.position) {
513
533
  position.set(...node.position);
@@ -1952,6 +1972,458 @@ var EdgeRenderer = class {
1952
1972
  arrow.quaternion.setFromUnitVectors(new THREE3__namespace.Vector3(0, 1, 0), direction);
1953
1973
  }
1954
1974
  };
1975
+ function computeConvexHull2D(points) {
1976
+ if (points.length < 3) {
1977
+ return points.map((p) => new THREE3__namespace.Vector2(p.x, p.y));
1978
+ }
1979
+ const points2D = points.map((p) => new THREE3__namespace.Vector2(p.x, p.y));
1980
+ let minIdx = 0;
1981
+ for (let i = 1; i < points2D.length; i++) {
1982
+ if (points2D[i].y < points2D[minIdx].y || points2D[i].y === points2D[minIdx].y && points2D[i].x < points2D[minIdx].x) {
1983
+ minIdx = i;
1984
+ }
1985
+ }
1986
+ [points2D[0], points2D[minIdx]] = [points2D[minIdx], points2D[0]];
1987
+ const pivot = points2D[0];
1988
+ const rest = points2D.slice(1).sort((a, b) => {
1989
+ const angleA = Math.atan2(a.y - pivot.y, a.x - pivot.x);
1990
+ const angleB = Math.atan2(b.y - pivot.y, b.x - pivot.x);
1991
+ if (angleA !== angleB) return angleA - angleB;
1992
+ return a.distanceTo(pivot) - b.distanceTo(pivot);
1993
+ });
1994
+ const hull = [pivot];
1995
+ for (const point of rest) {
1996
+ while (hull.length > 1) {
1997
+ const top = hull[hull.length - 1];
1998
+ const nextToTop = hull[hull.length - 2];
1999
+ const cross = (top.x - nextToTop.x) * (point.y - nextToTop.y) - (top.y - nextToTop.y) * (point.x - nextToTop.x);
2000
+ if (cross <= 0) {
2001
+ hull.pop();
2002
+ } else {
2003
+ break;
2004
+ }
2005
+ }
2006
+ hull.push(point);
2007
+ }
2008
+ return hull;
2009
+ }
2010
+ function expandHull(hull, padding) {
2011
+ if (hull.length < 3 || padding <= 0) return hull;
2012
+ const centroid = new THREE3__namespace.Vector2(0, 0);
2013
+ for (const p of hull) {
2014
+ centroid.add(p);
2015
+ }
2016
+ centroid.divideScalar(hull.length);
2017
+ return hull.map((p) => {
2018
+ const dir = new THREE3__namespace.Vector2().subVectors(p, centroid).normalize();
2019
+ return new THREE3__namespace.Vector2(p.x + dir.x * padding, p.y + dir.y * padding);
2020
+ });
2021
+ }
2022
+ var ClusterRenderer = class {
2023
+ constructor(scene, config = {}) {
2024
+ this.scene = scene;
2025
+ this.config = {
2026
+ defaultColor: config.defaultColor ?? "#4a5568",
2027
+ fillOpacity: config.fillOpacity ?? 0.08,
2028
+ strokeOpacity: config.strokeOpacity ?? 0.25,
2029
+ strokeWidth: config.strokeWidth ?? 1.5,
2030
+ labelFontFamily: config.labelFontFamily ?? "system-ui, sans-serif",
2031
+ labelFontSize: config.labelFontSize ?? 11,
2032
+ labelTextColor: config.labelTextColor ?? "#ffffff",
2033
+ labelBackground: config.labelBackground ?? "rgba(0, 0, 0, 0.6)",
2034
+ transitionsEnabled: config.transitionsEnabled ?? true,
2035
+ transitionDurationMs: config.transitionDurationMs ?? 300,
2036
+ zOffset: config.zOffset ?? -0.5,
2037
+ hullPadding: config.hullPadding ?? 1.2
2038
+ };
2039
+ this.scene.add(this.group);
2040
+ }
2041
+ group = new THREE3__namespace.Group();
2042
+ clusterStates = /* @__PURE__ */ new Map();
2043
+ config;
2044
+ /**
2045
+ * Render clusters with convex hull boundaries
2046
+ */
2047
+ renderClusters(clusters, nodePositions) {
2048
+ const currentIds = new Set(clusters.map((c) => c.id));
2049
+ for (const [id, state] of this.clusterStates) {
2050
+ if (!currentIds.has(id)) {
2051
+ this.removeClusterState(state);
2052
+ this.clusterStates.delete(id);
2053
+ }
2054
+ }
2055
+ for (const cluster of clusters) {
2056
+ const memberPositions = this.getMemberPositions(cluster.nodeIds, nodePositions);
2057
+ if (memberPositions.length < 1) {
2058
+ continue;
2059
+ }
2060
+ let state = this.clusterStates.get(cluster.id);
2061
+ if (!state) {
2062
+ state = this.createClusterState(cluster);
2063
+ this.clusterStates.set(cluster.id, state);
2064
+ }
2065
+ this.updateClusterGeometry(state, cluster, memberPositions);
2066
+ }
2067
+ }
2068
+ /**
2069
+ * Update cluster positions when nodes move (e.g., ambient motion)
2070
+ */
2071
+ updatePositions(nodePositions) {
2072
+ for (const [clusterId, state] of this.clusterStates) {
2073
+ let hasChanged = false;
2074
+ for (const [nodeId, lastPos] of state.lastNodePositions) {
2075
+ const currentPos = nodePositions.get(nodeId);
2076
+ if (currentPos && !currentPos.equals(lastPos)) {
2077
+ hasChanged = true;
2078
+ break;
2079
+ }
2080
+ }
2081
+ if (hasChanged) {
2082
+ const nodeIds = Array.from(state.lastNodePositions.keys());
2083
+ const memberPositions = this.getMemberPositions(nodeIds, nodePositions);
2084
+ if (memberPositions.length > 0) {
2085
+ this.updateClusterGeometry(state, { id: clusterId, label: "", nodeIds }, memberPositions);
2086
+ }
2087
+ }
2088
+ }
2089
+ }
2090
+ /**
2091
+ * Update method called each frame
2092
+ */
2093
+ update(_delta, _elapsed) {
2094
+ }
2095
+ /**
2096
+ * Clear all clusters
2097
+ */
2098
+ clear() {
2099
+ for (const state of this.clusterStates.values()) {
2100
+ this.removeClusterState(state);
2101
+ }
2102
+ this.clusterStates.clear();
2103
+ }
2104
+ /**
2105
+ * Dispose of all resources
2106
+ */
2107
+ dispose() {
2108
+ this.clear();
2109
+ this.scene.remove(this.group);
2110
+ }
2111
+ getMemberPositions(nodeIds, nodePositions) {
2112
+ const positions = [];
2113
+ for (const nodeId of nodeIds) {
2114
+ const pos = nodePositions.get(nodeId);
2115
+ if (pos) {
2116
+ positions.push(pos.clone());
2117
+ }
2118
+ }
2119
+ return positions;
2120
+ }
2121
+ createClusterState(cluster) {
2122
+ const color = new THREE3__namespace.Color(cluster.color ?? this.config.defaultColor);
2123
+ const labelElement = document.createElement("div");
2124
+ labelElement.className = "neuron-cluster-label";
2125
+ labelElement.style.cssText = `
2126
+ font-family: ${this.config.labelFontFamily};
2127
+ font-size: ${this.config.labelFontSize}px;
2128
+ font-weight: 500;
2129
+ color: ${this.config.labelTextColor};
2130
+ background: ${this.config.labelBackground};
2131
+ padding: 4px 10px;
2132
+ border-radius: 12px;
2133
+ white-space: nowrap;
2134
+ pointer-events: none;
2135
+ user-select: none;
2136
+ opacity: 0.85;
2137
+ text-transform: uppercase;
2138
+ letter-spacing: 0.5px;
2139
+ `;
2140
+ labelElement.textContent = cluster.label;
2141
+ const label = new CSS2DRenderer_js.CSS2DObject(labelElement);
2142
+ label.position.set(0, 0, 0);
2143
+ this.group.add(label);
2144
+ return {
2145
+ id: cluster.id,
2146
+ mesh: null,
2147
+ outline: null,
2148
+ label,
2149
+ labelElement,
2150
+ color,
2151
+ centroid: new THREE3__namespace.Vector3(),
2152
+ lastNodePositions: /* @__PURE__ */ new Map()
2153
+ };
2154
+ }
2155
+ updateClusterGeometry(state, cluster, memberPositions) {
2156
+ if (cluster.color) {
2157
+ state.color.set(cluster.color);
2158
+ }
2159
+ if (state.labelElement && cluster.label) {
2160
+ state.labelElement.textContent = cluster.label;
2161
+ }
2162
+ state.lastNodePositions.clear();
2163
+ for (let i = 0; i < cluster.nodeIds.length; i++) {
2164
+ if (memberPositions[i]) {
2165
+ state.lastNodePositions.set(cluster.nodeIds[i], memberPositions[i].clone());
2166
+ }
2167
+ }
2168
+ state.centroid.set(0, 0, 0);
2169
+ for (const pos of memberPositions) {
2170
+ state.centroid.add(pos);
2171
+ }
2172
+ state.centroid.divideScalar(memberPositions.length);
2173
+ if (cluster.position) {
2174
+ state.centroid.set(cluster.position.x, cluster.position.y, cluster.position.z);
2175
+ }
2176
+ if (state.label) {
2177
+ state.label.position.copy(state.centroid);
2178
+ state.label.position.y += 1.5;
2179
+ }
2180
+ if (memberPositions.length < 3) {
2181
+ this.removeClusterMesh(state);
2182
+ return;
2183
+ }
2184
+ const hull2D = computeConvexHull2D(memberPositions);
2185
+ const expandedHull = expandHull(hull2D, this.config.hullPadding ?? 1.2);
2186
+ if (expandedHull.length < 3) {
2187
+ this.removeClusterMesh(state);
2188
+ return;
2189
+ }
2190
+ const shape = new THREE3__namespace.Shape(expandedHull);
2191
+ const geometry = new THREE3__namespace.ShapeGeometry(shape);
2192
+ const avgZ = memberPositions.reduce((sum, p) => sum + p.z, 0) / memberPositions.length + (this.config.zOffset ?? -0.5);
2193
+ if (state.mesh) {
2194
+ state.mesh.geometry.dispose();
2195
+ state.mesh.geometry = geometry;
2196
+ state.mesh.position.z = avgZ;
2197
+ state.mesh.material.color.copy(state.color);
2198
+ } else {
2199
+ const material = new THREE3__namespace.MeshBasicMaterial({
2200
+ color: state.color,
2201
+ transparent: true,
2202
+ opacity: this.config.fillOpacity,
2203
+ side: THREE3__namespace.DoubleSide,
2204
+ depthWrite: false
2205
+ });
2206
+ state.mesh = new THREE3__namespace.Mesh(geometry, material);
2207
+ state.mesh.position.z = avgZ;
2208
+ state.mesh.renderOrder = -1;
2209
+ this.group.add(state.mesh);
2210
+ }
2211
+ const outlinePoints = [...expandedHull, expandedHull[0]].map(
2212
+ (p) => new THREE3__namespace.Vector3(p.x, p.y, avgZ + 0.01)
2213
+ );
2214
+ if (state.outline) {
2215
+ state.outline.geometry.dispose();
2216
+ state.outline.geometry = new THREE3__namespace.BufferGeometry().setFromPoints(outlinePoints);
2217
+ state.outline.material.color.copy(state.color);
2218
+ } else {
2219
+ const outlineGeometry = new THREE3__namespace.BufferGeometry().setFromPoints(outlinePoints);
2220
+ const outlineMaterial = new THREE3__namespace.LineBasicMaterial({
2221
+ color: state.color,
2222
+ transparent: true,
2223
+ opacity: this.config.strokeOpacity,
2224
+ linewidth: this.config.strokeWidth
2225
+ });
2226
+ state.outline = new THREE3__namespace.Line(outlineGeometry, outlineMaterial);
2227
+ state.outline.renderOrder = -1;
2228
+ this.group.add(state.outline);
2229
+ }
2230
+ }
2231
+ removeClusterMesh(state) {
2232
+ if (state.mesh) {
2233
+ state.mesh.geometry.dispose();
2234
+ state.mesh.material.dispose();
2235
+ this.group.remove(state.mesh);
2236
+ state.mesh = null;
2237
+ }
2238
+ if (state.outline) {
2239
+ state.outline.geometry.dispose();
2240
+ state.outline.material.dispose();
2241
+ this.group.remove(state.outline);
2242
+ state.outline = null;
2243
+ }
2244
+ }
2245
+ removeClusterState(state) {
2246
+ this.removeClusterMesh(state);
2247
+ if (state.label) {
2248
+ if (state.labelElement?.parentNode) {
2249
+ state.labelElement.parentNode.removeChild(state.labelElement);
2250
+ }
2251
+ this.group.remove(state.label);
2252
+ }
2253
+ }
2254
+ /**
2255
+ * Get cluster visibility state for external use
2256
+ */
2257
+ getClusterIds() {
2258
+ return Array.from(this.clusterStates.keys());
2259
+ }
2260
+ /**
2261
+ * Get centroid position for a cluster
2262
+ */
2263
+ getClusterCentroid(clusterId) {
2264
+ const state = this.clusterStates.get(clusterId);
2265
+ return state ? state.centroid.clone() : null;
2266
+ }
2267
+ };
2268
+
2269
+ // src/visualization/layouts/tree-layout.ts
2270
+ function applyTreeLayout(nodes, edges, options = {}) {
2271
+ if (nodes.length === 0) return nodes;
2272
+ const treeOptions = options.tree ?? {};
2273
+ const horizontalSpacing = treeOptions.horizontalSpacing ?? 3;
2274
+ const verticalSpacing = treeOptions.verticalSpacing ?? 4;
2275
+ const direction = treeOptions.direction ?? "down";
2276
+ const rootNodeId = treeOptions.rootNodeId;
2277
+ const nodeById = /* @__PURE__ */ new Map();
2278
+ const nodeBySlug = /* @__PURE__ */ new Map();
2279
+ for (const node of nodes) {
2280
+ nodeById.set(node.id, node);
2281
+ if (node.slug) {
2282
+ nodeBySlug.set(node.slug, node);
2283
+ }
2284
+ }
2285
+ const parentMap = /* @__PURE__ */ new Map();
2286
+ const childrenMap = /* @__PURE__ */ new Map();
2287
+ for (const edge of edges) {
2288
+ const fromNode = nodeBySlug.get(edge.from) ?? nodeById.get(edge.from);
2289
+ const toNode = nodeBySlug.get(edge.to) ?? nodeById.get(edge.to);
2290
+ if (!fromNode || !toNode) continue;
2291
+ const parentId = fromNode.id;
2292
+ const childId = toNode.id;
2293
+ if (!parentMap.has(childId)) {
2294
+ parentMap.set(childId, parentId);
2295
+ const siblings = childrenMap.get(parentId) ?? [];
2296
+ siblings.push(childId);
2297
+ childrenMap.set(parentId, siblings);
2298
+ }
2299
+ }
2300
+ let roots;
2301
+ if (rootNodeId) {
2302
+ const rootNode = nodeBySlug.get(rootNodeId) ?? nodeById.get(rootNodeId);
2303
+ roots = rootNode ? [rootNode.id] : [];
2304
+ } else {
2305
+ roots = nodes.filter((n) => !parentMap.has(n.id)).map((n) => n.id);
2306
+ }
2307
+ if (roots.length === 0 && nodes.length > 0) {
2308
+ roots = [nodes[0].id];
2309
+ }
2310
+ const treeNodes = /* @__PURE__ */ new Map();
2311
+ function calculateWidth(nodeId, depth) {
2312
+ const children = childrenMap.get(nodeId) ?? [];
2313
+ let width;
2314
+ if (children.length === 0) {
2315
+ width = 1;
2316
+ } else {
2317
+ width = 0;
2318
+ for (const childId of children) {
2319
+ width += calculateWidth(childId, depth + 1);
2320
+ }
2321
+ }
2322
+ treeNodes.set(nodeId, {
2323
+ id: nodeId,
2324
+ children,
2325
+ width,
2326
+ depth,
2327
+ x: 0
2328
+ });
2329
+ return width;
2330
+ }
2331
+ let totalWidth = 0;
2332
+ for (const rootId of roots) {
2333
+ totalWidth += calculateWidth(rootId, 0);
2334
+ }
2335
+ function assignPositions(nodeId, leftBound) {
2336
+ const treeNode = treeNodes.get(nodeId);
2337
+ if (!treeNode) return;
2338
+ const children = treeNode.children;
2339
+ if (children.length === 0) {
2340
+ treeNode.x = leftBound + 0.5;
2341
+ } else {
2342
+ let childLeft = leftBound;
2343
+ for (const childId of children) {
2344
+ assignPositions(childId, childLeft);
2345
+ const childNode = treeNodes.get(childId);
2346
+ if (childNode) {
2347
+ childLeft += childNode.width;
2348
+ }
2349
+ }
2350
+ const firstChild = treeNodes.get(children[0]);
2351
+ const lastChild = treeNodes.get(children[children.length - 1]);
2352
+ if (firstChild && lastChild) {
2353
+ treeNode.x = (firstChild.x + lastChild.x) / 2;
2354
+ }
2355
+ }
2356
+ }
2357
+ let currentLeft = 0;
2358
+ for (const rootId of roots) {
2359
+ assignPositions(rootId, currentLeft);
2360
+ const rootTree = treeNodes.get(rootId);
2361
+ if (rootTree) {
2362
+ currentLeft += rootTree.width;
2363
+ }
2364
+ }
2365
+ const positions = /* @__PURE__ */ new Map();
2366
+ const centerOffset = totalWidth / 2;
2367
+ const isHorizontal = direction === "left" || direction === "right";
2368
+ for (const [nodeId, treeNode] of treeNodes) {
2369
+ const siblingPos = (treeNode.x - centerOffset) * horizontalSpacing;
2370
+ let depthPos;
2371
+ switch (direction) {
2372
+ case "up":
2373
+ depthPos = treeNode.depth * verticalSpacing;
2374
+ break;
2375
+ case "left":
2376
+ depthPos = -treeNode.depth * verticalSpacing;
2377
+ break;
2378
+ case "right":
2379
+ depthPos = treeNode.depth * verticalSpacing;
2380
+ break;
2381
+ case "down":
2382
+ default:
2383
+ depthPos = -treeNode.depth * verticalSpacing;
2384
+ break;
2385
+ }
2386
+ const x = isHorizontal ? depthPos : siblingPos;
2387
+ const y = isHorizontal ? siblingPos : depthPos;
2388
+ positions.set(nodeId, [x, y, 0]);
2389
+ }
2390
+ const positionedIds = new Set(positions.keys());
2391
+ const orphans = nodes.filter((n) => !positionedIds.has(n.id));
2392
+ if (orphans.length > 0) {
2393
+ const maxDepth = Math.max(...Array.from(treeNodes.values()).map((t) => t.depth), 0);
2394
+ const orphanDepth = (maxDepth + 2) * verticalSpacing;
2395
+ const orphanSpread = (orphans.length - 1) * horizontalSpacing / 2;
2396
+ orphans.forEach((orphan, index) => {
2397
+ const siblingPos = -orphanSpread + index * horizontalSpacing;
2398
+ let depthPos;
2399
+ switch (direction) {
2400
+ case "up":
2401
+ depthPos = orphanDepth;
2402
+ break;
2403
+ case "left":
2404
+ depthPos = -orphanDepth;
2405
+ break;
2406
+ case "right":
2407
+ depthPos = orphanDepth;
2408
+ break;
2409
+ case "down":
2410
+ default:
2411
+ depthPos = -orphanDepth;
2412
+ break;
2413
+ }
2414
+ const x = isHorizontal ? depthPos : siblingPos;
2415
+ const y = isHorizontal ? siblingPos : depthPos;
2416
+ positions.set(orphan.id, [x, y, 0]);
2417
+ });
2418
+ }
2419
+ return nodes.map((node) => {
2420
+ const position = positions.get(node.id);
2421
+ if (position) {
2422
+ return { ...node, position };
2423
+ }
2424
+ return node;
2425
+ });
2426
+ }
1955
2427
 
1956
2428
  // src/visualization/layouts/fuzzy-layout.ts
1957
2429
  var GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5));
@@ -2004,11 +2476,14 @@ function mulberry32(seed) {
2004
2476
  function buildSeed(baseSeed, nodeKey) {
2005
2477
  return mulberry32(hashString(`${baseSeed}:${nodeKey}`));
2006
2478
  }
2007
- function applyFuzzyLayout(nodes, options = {}) {
2479
+ function applyFuzzyLayout(nodes, options = {}, edges) {
2008
2480
  const mode = options.mode ?? "atlas";
2009
2481
  if (mode === "positioned") {
2010
2482
  return nodes;
2011
2483
  }
2484
+ if (mode === "tree") {
2485
+ return applyTreeLayout(nodes, edges ?? [], options);
2486
+ }
2012
2487
  const needsLayout = nodes.some((node) => !node.position);
2013
2488
  if (mode === "auto" && !needsLayout) {
2014
2489
  return nodes;
@@ -2674,6 +3149,7 @@ function NeuronWeb({
2674
3149
  const transitionsEnabled = resolvedPerformanceMode === "normal" && !prefersReducedMotion && profileAllowsContinuous && resolvedAnimationConfig.transitionDurationMs > 0;
2675
3150
  return new NodeRenderer(sceneManager.scene, {
2676
3151
  domainColors: resolvedTheme.colors.domainColors,
3152
+ statusColors: resolvedTheme.colors.statusColors,
2677
3153
  defaultColor: resolvedTheme.colors.defaultDomainColor,
2678
3154
  baseScale: 1.15,
2679
3155
  tierScales: {
@@ -2778,6 +3254,27 @@ function NeuronWeb({
2778
3254
  edgeRenderer?.dispose();
2779
3255
  };
2780
3256
  }, [edgeRenderer]);
3257
+ const clusterRenderer = react.useMemo(() => {
3258
+ if (!sceneManager) return null;
3259
+ if (!graphData.clusters?.length) return null;
3260
+ return new ClusterRenderer(sceneManager.scene, {
3261
+ defaultColor: resolvedTheme.colors.defaultDomainColor,
3262
+ fillOpacity: 0.08,
3263
+ strokeOpacity: 0.25,
3264
+ strokeWidth: 1.5,
3265
+ labelFontFamily: resolvedTheme.typography.labelFontFamily,
3266
+ labelFontSize: resolvedTheme.typography.labelFontSize - 1,
3267
+ labelTextColor: resolvedTheme.colors.labelText,
3268
+ labelBackground: resolvedTheme.colors.labelBackground,
3269
+ zOffset: -0.5,
3270
+ hullPadding: 1.2
3271
+ });
3272
+ }, [sceneManager, graphData.clusters?.length, resolvedTheme]);
3273
+ react.useEffect(() => {
3274
+ return () => {
3275
+ clusterRenderer?.dispose();
3276
+ };
3277
+ }, [clusterRenderer]);
2781
3278
  const doubleClickEnabled = false;
2782
3279
  const interactionManager = react.useMemo(() => {
2783
3280
  if (!sceneManager) return null;
@@ -2820,8 +3317,8 @@ function NeuronWeb({
2820
3317
  [layout, resolvedDensity.spread]
2821
3318
  );
2822
3319
  const resolvedNodes = react.useMemo(
2823
- () => applyFuzzyLayout(workingGraph.nodes, layoutOptions),
2824
- [workingGraph.nodes, layoutOptions]
3320
+ () => applyFuzzyLayout(workingGraph.nodes, layoutOptions, workingGraph.edges),
3321
+ [workingGraph.nodes, workingGraph.edges, layoutOptions]
2825
3322
  );
2826
3323
  const displayNodes = react.useMemo(() => {
2827
3324
  if (!selectedNodeId || !resolvedDensity.focusExpansion) return resolvedNodes;
@@ -3203,7 +3700,18 @@ function NeuronWeb({
3203
3700
  nodeRenderer.renderNodes(displayNodes);
3204
3701
  const positions = nodeRenderer.getNodePositionsBySlug(/* @__PURE__ */ new Map());
3205
3702
  edgeRenderer.renderEdges(workingGraph.edges, positions);
3206
- }, [displayNodes, workingGraph.edges, sceneManager, nodeRenderer, edgeRenderer]);
3703
+ if (clusterRenderer && graphData.clusters?.length) {
3704
+ const nodePositionsById = /* @__PURE__ */ new Map();
3705
+ for (const node of displayNodes) {
3706
+ const pos = nodeRenderer.getNodePosition(node.id);
3707
+ if (pos) {
3708
+ nodePositionsById.set(node.id, pos);
3709
+ nodePositionsById.set(node.slug, pos);
3710
+ }
3711
+ }
3712
+ clusterRenderer.renderClusters(graphData.clusters, nodePositionsById);
3713
+ }
3714
+ }, [displayNodes, workingGraph.edges, graphData.clusters, sceneManager, nodeRenderer, edgeRenderer, clusterRenderer]);
3207
3715
  react.useEffect(() => {
3208
3716
  if (!sceneManager) return;
3209
3717
  sceneManager.updateBackground(resolvedTheme.colors.background);
@@ -4126,6 +4634,7 @@ function dedupePreserveOrder(values) {
4126
4634
  }
4127
4635
 
4128
4636
  exports.DEFAULT_RENDERING_OPTIONS = DEFAULT_RENDERING_OPTIONS;
4637
+ exports.DEFAULT_STATUS_COLORS = DEFAULT_STATUS_COLORS;
4129
4638
  exports.DEFAULT_THEME = DEFAULT_THEME;
4130
4639
  exports.NeuronContext = NeuronContext;
4131
4640
  exports.NeuronWeb = NeuronWeb;
@@ -4133,6 +4642,7 @@ exports.NeuronWebExplorer = NeuronWebExplorer;
4133
4642
  exports.SceneManager = SceneManager;
4134
4643
  exports.ThemeEngine = ThemeEngine;
4135
4644
  exports.applyFuzzyLayout = applyFuzzyLayout;
4645
+ exports.applyTreeLayout = applyTreeLayout;
4136
4646
  exports.createStoryBeat = createStoryBeat;
4137
4647
  exports.createStudyPathFromBeat = createStudyPathFromBeat;
4138
4648
  exports.createStudyPathFromNodeIds = createStudyPathFromNodeIds;
@@ -4141,5 +4651,5 @@ exports.normalizeStoryBeat = normalizeStoryBeat;
4141
4651
  exports.useNeuronContext = useNeuronContext;
4142
4652
  exports.useNeuronGraph = useNeuronGraph;
4143
4653
  exports.validateStoryBeat = validateStoryBeat;
4144
- //# sourceMappingURL=chunk-5SZ37JXQ.cjs.map
4145
- //# sourceMappingURL=chunk-5SZ37JXQ.cjs.map
4654
+ //# sourceMappingURL=chunk-A5KXAYKG.cjs.map
4655
+ //# sourceMappingURL=chunk-A5KXAYKG.cjs.map