@omiron33/omi-neuron-web 0.2.21 → 0.2.22

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,299 @@ 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
+ };
1955
2268
 
1956
2269
  // src/visualization/layouts/fuzzy-layout.ts
1957
2270
  var GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5));
@@ -2674,6 +2987,7 @@ function NeuronWeb({
2674
2987
  const transitionsEnabled = resolvedPerformanceMode === "normal" && !prefersReducedMotion && profileAllowsContinuous && resolvedAnimationConfig.transitionDurationMs > 0;
2675
2988
  return new NodeRenderer(sceneManager.scene, {
2676
2989
  domainColors: resolvedTheme.colors.domainColors,
2990
+ statusColors: resolvedTheme.colors.statusColors,
2677
2991
  defaultColor: resolvedTheme.colors.defaultDomainColor,
2678
2992
  baseScale: 1.15,
2679
2993
  tierScales: {
@@ -2778,6 +3092,27 @@ function NeuronWeb({
2778
3092
  edgeRenderer?.dispose();
2779
3093
  };
2780
3094
  }, [edgeRenderer]);
3095
+ const clusterRenderer = react.useMemo(() => {
3096
+ if (!sceneManager) return null;
3097
+ if (!graphData.clusters?.length) return null;
3098
+ return new ClusterRenderer(sceneManager.scene, {
3099
+ defaultColor: resolvedTheme.colors.defaultDomainColor,
3100
+ fillOpacity: 0.08,
3101
+ strokeOpacity: 0.25,
3102
+ strokeWidth: 1.5,
3103
+ labelFontFamily: resolvedTheme.typography.labelFontFamily,
3104
+ labelFontSize: resolvedTheme.typography.labelFontSize - 1,
3105
+ labelTextColor: resolvedTheme.colors.labelText,
3106
+ labelBackground: resolvedTheme.colors.labelBackground,
3107
+ zOffset: -0.5,
3108
+ hullPadding: 1.2
3109
+ });
3110
+ }, [sceneManager, graphData.clusters?.length, resolvedTheme]);
3111
+ react.useEffect(() => {
3112
+ return () => {
3113
+ clusterRenderer?.dispose();
3114
+ };
3115
+ }, [clusterRenderer]);
2781
3116
  const doubleClickEnabled = false;
2782
3117
  const interactionManager = react.useMemo(() => {
2783
3118
  if (!sceneManager) return null;
@@ -3203,7 +3538,18 @@ function NeuronWeb({
3203
3538
  nodeRenderer.renderNodes(displayNodes);
3204
3539
  const positions = nodeRenderer.getNodePositionsBySlug(/* @__PURE__ */ new Map());
3205
3540
  edgeRenderer.renderEdges(workingGraph.edges, positions);
3206
- }, [displayNodes, workingGraph.edges, sceneManager, nodeRenderer, edgeRenderer]);
3541
+ if (clusterRenderer && graphData.clusters?.length) {
3542
+ const nodePositionsById = /* @__PURE__ */ new Map();
3543
+ for (const node of displayNodes) {
3544
+ const pos = nodeRenderer.getNodePosition(node.id);
3545
+ if (pos) {
3546
+ nodePositionsById.set(node.id, pos);
3547
+ nodePositionsById.set(node.slug, pos);
3548
+ }
3549
+ }
3550
+ clusterRenderer.renderClusters(graphData.clusters, nodePositionsById);
3551
+ }
3552
+ }, [displayNodes, workingGraph.edges, graphData.clusters, sceneManager, nodeRenderer, edgeRenderer, clusterRenderer]);
3207
3553
  react.useEffect(() => {
3208
3554
  if (!sceneManager) return;
3209
3555
  sceneManager.updateBackground(resolvedTheme.colors.background);
@@ -4126,6 +4472,7 @@ function dedupePreserveOrder(values) {
4126
4472
  }
4127
4473
 
4128
4474
  exports.DEFAULT_RENDERING_OPTIONS = DEFAULT_RENDERING_OPTIONS;
4475
+ exports.DEFAULT_STATUS_COLORS = DEFAULT_STATUS_COLORS;
4129
4476
  exports.DEFAULT_THEME = DEFAULT_THEME;
4130
4477
  exports.NeuronContext = NeuronContext;
4131
4478
  exports.NeuronWeb = NeuronWeb;
@@ -4141,5 +4488,5 @@ exports.normalizeStoryBeat = normalizeStoryBeat;
4141
4488
  exports.useNeuronContext = useNeuronContext;
4142
4489
  exports.useNeuronGraph = useNeuronGraph;
4143
4490
  exports.validateStoryBeat = validateStoryBeat;
4144
- //# sourceMappingURL=chunk-5SZ37JXQ.cjs.map
4145
- //# sourceMappingURL=chunk-5SZ37JXQ.cjs.map
4491
+ //# sourceMappingURL=chunk-HOW2F2KP.cjs.map
4492
+ //# sourceMappingURL=chunk-HOW2F2KP.cjs.map