@backbay/glia-three 0.2.0-alpha.2

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.
Files changed (172) hide show
  1. package/package.json +43 -0
  2. package/src/environment/AuroraLayer/AuroraLayer.stories.tsx +43 -0
  3. package/src/environment/AuroraLayer/AuroraLayer.tsx +200 -0
  4. package/src/environment/AuroraLayer/index.ts +2 -0
  5. package/src/environment/AuroraLayer/types.ts +30 -0
  6. package/src/environment/EnvironmentLayer/EnvironmentLayer.stories.tsx +262 -0
  7. package/src/environment/EnvironmentLayer/EnvironmentLayer.tsx +105 -0
  8. package/src/environment/EnvironmentLayer/index.ts +4 -0
  9. package/src/environment/EnvironmentLayer/presets.ts +128 -0
  10. package/src/environment/FogLayer/FogLayer.stories.tsx +83 -0
  11. package/src/environment/FogLayer/FogLayer.tsx +113 -0
  12. package/src/environment/FogLayer/index.ts +2 -0
  13. package/src/environment/FogLayer/types.ts +45 -0
  14. package/src/environment/VolumetricLight/VolumetricLight.stories.tsx +55 -0
  15. package/src/environment/VolumetricLight/VolumetricLight.tsx +188 -0
  16. package/src/environment/VolumetricLight/index.ts +2 -0
  17. package/src/environment/VolumetricLight/types.ts +33 -0
  18. package/src/environment/WeatherLayer/WeatherLayer.stories.tsx +348 -0
  19. package/src/environment/WeatherLayer/WeatherLayer.tsx +266 -0
  20. package/src/environment/WeatherLayer/cinematicCanvas.tsx +809 -0
  21. package/src/environment/WeatherLayer/colors.ts +27 -0
  22. package/src/environment/WeatherLayer/index.ts +4 -0
  23. package/src/environment/WeatherLayer/leafPresets.ts +12 -0
  24. package/src/environment/WeatherLayer/particles.ts +227 -0
  25. package/src/environment/WeatherLayer/types.ts +140 -0
  26. package/src/environment/index.ts +17 -0
  27. package/src/environment/shared/index.ts +2 -0
  28. package/src/environment/shared/noise.ts +10 -0
  29. package/src/environment/shared/performance.ts +33 -0
  30. package/src/environment/shared/types.ts +26 -0
  31. package/src/index.ts +2 -0
  32. package/src/lib/utils.ts +6 -0
  33. package/src/lib/vision-types.ts +84 -0
  34. package/src/three/AgentConsole/AgentConsole.stories.tsx +397 -0
  35. package/src/three/AgentConsole/AgentConsole.tsx +195 -0
  36. package/src/three/AgentConsole/ConsoleChat.tsx +237 -0
  37. package/src/three/AgentConsole/FocusConstellation.tsx +297 -0
  38. package/src/three/AgentConsole/GlyphAvatar.stories.tsx +110 -0
  39. package/src/three/AgentConsole/GlyphAvatar.tsx +117 -0
  40. package/src/three/AgentConsole/QuickActions.tsx +111 -0
  41. package/src/three/AgentConsole/index.ts +41 -0
  42. package/src/three/AgentConsole/types.ts +241 -0
  43. package/src/three/AmbientField/AmbientField.stories.tsx +290 -0
  44. package/src/three/AmbientField/AmbientField.tsx +307 -0
  45. package/src/three/AmbientField/BackbayFieldBus.ts +326 -0
  46. package/src/three/AmbientField/FieldProvider.tsx +83 -0
  47. package/src/three/AmbientField/index.ts +37 -0
  48. package/src/three/AmbientField/shaders/constellation.ts +384 -0
  49. package/src/three/AmbientField/types.ts +174 -0
  50. package/src/three/AttackGraph/AttackGraph.stories.tsx +144 -0
  51. package/src/three/AttackGraph/AttackGraph.tsx +325 -0
  52. package/src/three/AttackGraph/index.ts +19 -0
  53. package/src/three/AttackGraph/types.ts +97 -0
  54. package/src/three/AuditTrail/AuditTrail.stories.tsx +567 -0
  55. package/src/three/AuditTrail/AuditTrail.tsx +644 -0
  56. package/src/three/AuditTrail/index.ts +33 -0
  57. package/src/three/AuditTrail/types.ts +192 -0
  58. package/src/three/CrystallineOrganism/Breadcrumb.tsx +61 -0
  59. package/src/three/CrystallineOrganism/CrystallineOrganism.stories.tsx +509 -0
  60. package/src/three/CrystallineOrganism/CrystallineOrganism.tsx +273 -0
  61. package/src/three/CrystallineOrganism/LatticeEdge.tsx +69 -0
  62. package/src/three/CrystallineOrganism/OrganismLattice.tsx +159 -0
  63. package/src/three/CrystallineOrganism/OrganismParticles.tsx +148 -0
  64. package/src/three/CrystallineOrganism/OrganismShell.tsx +277 -0
  65. package/src/three/CrystallineOrganism/constants.ts +161 -0
  66. package/src/three/CrystallineOrganism/index.ts +17 -0
  67. package/src/three/CrystallineOrganism/layouts/hexGrid.ts +85 -0
  68. package/src/three/CrystallineOrganism/layouts/index.ts +1 -0
  69. package/src/three/CrystallineOrganism/types.ts +167 -0
  70. package/src/three/CrystallineOrganism/useOrganismEmotion.ts +84 -0
  71. package/src/three/FirewallBarrier/FirewallBarrier.stories.tsx +167 -0
  72. package/src/three/FirewallBarrier/FirewallBarrier.tsx +259 -0
  73. package/src/three/FirewallBarrier/index.ts +14 -0
  74. package/src/three/FirewallBarrier/types.ts +52 -0
  75. package/src/three/Glyph/GlyphObject.stories.tsx +577 -0
  76. package/src/three/Glyph/GlyphObject.tsx +422 -0
  77. package/src/three/Glyph/index.ts +10 -0
  78. package/src/three/Glyph/types.ts +36 -0
  79. package/src/three/Glyph/useGlyphController.ts +231 -0
  80. package/src/three/Glyph/useGlyphEmotion.ts +70 -0
  81. package/src/three/Graph3D/Graph3D.stories.tsx +269 -0
  82. package/src/three/Graph3D/Graph3D.tsx +248 -0
  83. package/src/three/Graph3D/GraphEdge.tsx +79 -0
  84. package/src/three/Graph3D/GraphNode.tsx +239 -0
  85. package/src/three/Graph3D/types.ts +66 -0
  86. package/src/three/Graph3D/utils.ts +204 -0
  87. package/src/three/IntelFeed/IntelFeed.stories.tsx +168 -0
  88. package/src/three/IntelFeed/IntelFeed.tsx +284 -0
  89. package/src/three/IntelFeed/index.ts +14 -0
  90. package/src/three/IntelFeed/types.ts +56 -0
  91. package/src/three/MetricsGalaxy/MetricsGalaxy.tsx +484 -0
  92. package/src/three/MetricsGalaxy/index.ts +6 -0
  93. package/src/three/MetricsGalaxy/types.ts +26 -0
  94. package/src/three/NetworkTopology/NetworkTopology.stories.tsx +184 -0
  95. package/src/three/NetworkTopology/NetworkTopology.tsx +421 -0
  96. package/src/three/NetworkTopology/index.ts +34 -0
  97. package/src/three/NetworkTopology/types.ts +128 -0
  98. package/src/three/ParticleField/ParticleField.stories.tsx +162 -0
  99. package/src/three/ParticleField/ParticleField.tsx +81 -0
  100. package/src/three/ParticleField/index.ts +1 -0
  101. package/src/three/PermissionMatrix/PermissionMatrix.stories.tsx +475 -0
  102. package/src/three/PermissionMatrix/PermissionMatrix.tsx +380 -0
  103. package/src/three/PermissionMatrix/index.ts +15 -0
  104. package/src/three/PermissionMatrix/types.ts +54 -0
  105. package/src/three/QuantumField/ConstellationField.tsx +238 -0
  106. package/src/three/QuantumField/FieldBus.ts +349 -0
  107. package/src/three/QuantumField/FieldLayer.tsx +430 -0
  108. package/src/three/QuantumField/FieldProvider.tsx +460 -0
  109. package/src/three/QuantumField/PcbField.tsx +406 -0
  110. package/src/three/QuantumField/QuantumField.stories.tsx +1155 -0
  111. package/src/three/QuantumField/TrailRTT.ts +212 -0
  112. package/src/three/QuantumField/WaterField.tsx +226 -0
  113. package/src/three/QuantumField/WaterSimRTT.ts +283 -0
  114. package/src/three/QuantumField/domMapping.ts +185 -0
  115. package/src/three/QuantumField/index.ts +110 -0
  116. package/src/three/QuantumField/styles/index.ts +9 -0
  117. package/src/three/QuantumField/styles/styleA.ts +526 -0
  118. package/src/three/QuantumField/styles/styleB.ts +1210 -0
  119. package/src/three/QuantumField/styles/styleC.ts +266 -0
  120. package/src/three/QuantumField/themes.ts +211 -0
  121. package/src/three/QuantumField/types.ts +380 -0
  122. package/src/three/SOCCommandCenter/SOCCommandCenter.stories.tsx +591 -0
  123. package/src/three/SOCCommandCenter/SOCCommandCenter.tsx +248 -0
  124. package/src/three/SOCCommandCenter/index.ts +26 -0
  125. package/src/three/SOCCommandCenter/types.ts +201 -0
  126. package/src/three/SecurityDashboard/SecurityDashboard.stories.tsx +508 -0
  127. package/src/three/SecurityDashboard/SecurityDashboard.tsx +507 -0
  128. package/src/three/SecurityDashboard/index.ts +37 -0
  129. package/src/three/SecurityDashboard/types.ts +143 -0
  130. package/src/three/SecurityShield/SecurityShield.stories.tsx +257 -0
  131. package/src/three/SecurityShield/SecurityShield.tsx +502 -0
  132. package/src/three/SecurityShield/index.ts +25 -0
  133. package/src/three/SecurityShield/types.ts +64 -0
  134. package/src/three/Sentinel/AvatarMode.tsx +578 -0
  135. package/src/three/Sentinel/AvatarRenderer.tsx +199 -0
  136. package/src/three/Sentinel/CameraPip.tsx +127 -0
  137. package/src/three/Sentinel/CardinalItem.tsx +83 -0
  138. package/src/three/Sentinel/CardinalMenu.tsx +370 -0
  139. package/src/three/Sentinel/DockedMiniOrb.tsx +146 -0
  140. package/src/three/Sentinel/RadialSubmenu.tsx +273 -0
  141. package/src/three/Sentinel/SentinelConversation.tsx +802 -0
  142. package/src/three/Sentinel/SentinelOrb.tsx +316 -0
  143. package/src/three/Sentinel/SentinelOverlay.tsx +146 -0
  144. package/src/three/Sentinel/SentinelProvider.tsx +145 -0
  145. package/src/three/Sentinel/SentinelTether.tsx +182 -0
  146. package/src/three/Sentinel/SigilPlaceholder.tsx +176 -0
  147. package/src/three/Sentinel/VerticalSubmenu.tsx +150 -0
  148. package/src/three/Sentinel/index.ts +145 -0
  149. package/src/three/Sentinel/sentinelStore.ts +196 -0
  150. package/src/three/Sentinel/types.ts +403 -0
  151. package/src/three/Sentinel/useCameraPermission.ts +153 -0
  152. package/src/three/Sentinel/useThrowPhysics.ts +220 -0
  153. package/src/three/SpatialWorkspace/CyntraWorkspace.tsx +84 -0
  154. package/src/three/SpatialWorkspace/JobCluster.tsx +281 -0
  155. package/src/three/SpatialWorkspace/NodeGraph.tsx +236 -0
  156. package/src/three/SpatialWorkspace/ReceiptOrbit.tsx +368 -0
  157. package/src/three/SpatialWorkspace/SpatialWorkspace.stories.tsx +547 -0
  158. package/src/three/SpatialWorkspace/SpatialWorkspace.tsx +428 -0
  159. package/src/three/SpatialWorkspace/TrustRings.tsx +228 -0
  160. package/src/three/SpatialWorkspace/adapters.ts +353 -0
  161. package/src/three/SpatialWorkspace/index.ts +85 -0
  162. package/src/three/SpatialWorkspace/nexusAdapter.ts +182 -0
  163. package/src/three/SpatialWorkspace/types.ts +389 -0
  164. package/src/three/ThreatRadar/ThreatRadar.stories.tsx +451 -0
  165. package/src/three/ThreatRadar/ThreatRadar.tsx +542 -0
  166. package/src/three/ThreatRadar/index.ts +8 -0
  167. package/src/three/ThreatRadar/types.ts +90 -0
  168. package/src/three/ThreeErrorBoundary/ThreeErrorBoundary.tsx +235 -0
  169. package/src/three/ThreeErrorBoundary/index.ts +5 -0
  170. package/src/three/index.ts +56 -0
  171. package/tsconfig.json +20 -0
  172. package/tsup.config.ts +21 -0
@@ -0,0 +1,484 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ThreeEvent, useFrame } from "@react-three/fiber";
5
+ import { Html, Line } from "@react-three/drei";
6
+ import * as THREE from "three";
7
+ import type {
8
+ MetricNode,
9
+ MetricConnection,
10
+ MetricsGalaxyProps,
11
+ } from "./types";
12
+
13
+ // --- Neon palette from design DNA ---
14
+ const NEON = {
15
+ cyan: "#22D3EE",
16
+ magenta: "#F43F5E",
17
+ emerald: "#10B981",
18
+ violet: "#8B5CF6",
19
+ yellow: "#EAB308",
20
+ red: "#EF4444",
21
+ };
22
+
23
+ // Category-to-color mapping (fallback cycles through neons)
24
+ const CATEGORY_COLORS: Record<string, string> = {
25
+ compute: NEON.cyan,
26
+ network: NEON.violet,
27
+ storage: NEON.magenta,
28
+ memory: NEON.emerald,
29
+ };
30
+
31
+ function getCategoryColor(category?: string): string {
32
+ if (category && CATEGORY_COLORS[category]) return CATEGORY_COLORS[category];
33
+ return NEON.cyan;
34
+ }
35
+
36
+ /**
37
+ * Determine the status color of a metric based on its threshold.
38
+ * Green (healthy) -> Yellow (warn) -> Red (critical)
39
+ */
40
+ function getStatusColor(metric: MetricNode): THREE.Color {
41
+ if (!metric.threshold) return new THREE.Color(NEON.emerald);
42
+
43
+ const { warn, critical } = metric.threshold;
44
+ if (metric.value >= critical) return new THREE.Color(NEON.red);
45
+ if (metric.value >= warn) return new THREE.Color(NEON.yellow);
46
+ return new THREE.Color(NEON.emerald);
47
+ }
48
+
49
+ /**
50
+ * Returns a sphere scale factor; larger when closer to critical threshold.
51
+ */
52
+ function getNodeScale(metric: MetricNode): number {
53
+ if (!metric.threshold) return 1;
54
+ const ratio = metric.value / metric.threshold.critical;
55
+ return 0.8 + Math.min(ratio, 1.5) * 0.7;
56
+ }
57
+
58
+ // --- Layout algorithms ---
59
+
60
+ function galaxyLayout(metrics: MetricNode[]): Map<string, THREE.Vector3> {
61
+ const map = new Map<string, THREE.Vector3>();
62
+ const phi = (1 + Math.sqrt(5)) / 2; // golden ratio
63
+ const n = metrics.length;
64
+ metrics.forEach((m, i) => {
65
+ const theta = (2 * Math.PI * i) / phi;
66
+ const r = 1.2 + Math.sqrt(i / n) * 3;
67
+ const y = (i / (n - 1 || 1) - 0.5) * 2;
68
+ map.set(m.id, new THREE.Vector3(Math.cos(theta) * r, y, Math.sin(theta) * r));
69
+ });
70
+ return map;
71
+ }
72
+
73
+ function gridLayout(metrics: MetricNode[]): Map<string, THREE.Vector3> {
74
+ const map = new Map<string, THREE.Vector3>();
75
+ const cols = Math.ceil(Math.sqrt(metrics.length));
76
+ const spacing = 2;
77
+ metrics.forEach((m, i) => {
78
+ const row = Math.floor(i / cols);
79
+ const col = i % cols;
80
+ const x = (col - (cols - 1) / 2) * spacing;
81
+ const z = (row - (Math.ceil(metrics.length / cols) - 1) / 2) * spacing;
82
+ map.set(m.id, new THREE.Vector3(x, 0, z));
83
+ });
84
+ return map;
85
+ }
86
+
87
+ function radialLayout(metrics: MetricNode[]): Map<string, THREE.Vector3> {
88
+ const map = new Map<string, THREE.Vector3>();
89
+ // Group by category
90
+ const groups = new Map<string, MetricNode[]>();
91
+ metrics.forEach((m) => {
92
+ const cat = m.category || "default";
93
+ if (!groups.has(cat)) groups.set(cat, []);
94
+ groups.get(cat)!.push(m);
95
+ });
96
+
97
+ const groupKeys = Array.from(groups.keys());
98
+ const sectorAngle = (2 * Math.PI) / groupKeys.length;
99
+
100
+ groupKeys.forEach((cat, gi) => {
101
+ const nodes = groups.get(cat)!;
102
+ const baseAngle = gi * sectorAngle;
103
+ nodes.forEach((m, ni) => {
104
+ const r = 1.5 + ni * 1.2;
105
+ const spread = sectorAngle * 0.3;
106
+ const angle = baseAngle + (ni / (nodes.length || 1) - 0.5) * spread;
107
+ map.set(m.id, new THREE.Vector3(Math.cos(angle) * r, 0, Math.sin(angle) * r));
108
+ });
109
+ });
110
+ return map;
111
+ }
112
+
113
+ function computeLayout(
114
+ metrics: MetricNode[],
115
+ layout: "galaxy" | "grid" | "radial"
116
+ ): Map<string, THREE.Vector3> {
117
+ switch (layout) {
118
+ case "grid":
119
+ return gridLayout(metrics);
120
+ case "radial":
121
+ return radialLayout(metrics);
122
+ default:
123
+ return galaxyLayout(metrics);
124
+ }
125
+ }
126
+
127
+ // --- Mini sparkline as a Line halo ---
128
+
129
+ function SparklineHalo({
130
+ history,
131
+ color,
132
+ radius,
133
+ }: {
134
+ history: number[];
135
+ color: THREE.Color;
136
+ radius: number;
137
+ }) {
138
+ const points = React.useMemo(() => {
139
+ if (history.length < 2) return null;
140
+ const min = Math.min(...history);
141
+ const max = Math.max(...history);
142
+ const range = max - min || 1;
143
+ const pts: [number, number, number][] = history.map((v, i) => {
144
+ const angle = (i / (history.length - 1)) * Math.PI * 2;
145
+ const normalized = (v - min) / range;
146
+ const r = radius + normalized * 0.15;
147
+ return [Math.cos(angle) * r, normalized * 0.1, Math.sin(angle) * r];
148
+ });
149
+ // Close the loop
150
+ pts.push(pts[0]);
151
+ return pts;
152
+ }, [history, radius]);
153
+
154
+ if (!points) return null;
155
+
156
+ return (
157
+ <Line
158
+ points={points}
159
+ color={color.getStyle()}
160
+ lineWidth={1.5}
161
+ transparent
162
+ opacity={0.5}
163
+ />
164
+ );
165
+ }
166
+
167
+ // --- Tooltip overlay ---
168
+
169
+ function MetricTooltip({ metric, color }: { metric: MetricNode; color: THREE.Color }) {
170
+ const trendArrow = metric.trend > 0 ? "\u25B2" : metric.trend < 0 ? "\u25BC" : "\u25CF";
171
+ const trendColor =
172
+ metric.trend > 0 ? NEON.emerald : metric.trend < 0 ? NEON.red : "rgba(100,116,139,1)";
173
+
174
+ return (
175
+ <Html position={[0, 0.6, 0]} center style={{ pointerEvents: "none" }}>
176
+ <div
177
+ style={{
178
+ background: "rgba(2,4,10,0.85)",
179
+ backdropFilter: "blur(24px)",
180
+ border: "1px solid rgba(255,255,255,0.06)",
181
+ boxShadow: `0 0 12px ${color.getStyle()}30, inset 0 1px 0 rgba(255,255,255,0.02)`,
182
+ padding: "8px 12px",
183
+ borderRadius: "6px",
184
+ minWidth: "120px",
185
+ fontFamily: "monospace",
186
+ fontSize: "10px",
187
+ letterSpacing: "0.12em",
188
+ textTransform: "uppercase" as const,
189
+ color: "rgba(226,232,240,1)",
190
+ }}
191
+ >
192
+ <div
193
+ style={{
194
+ fontWeight: 700,
195
+ fontSize: "11px",
196
+ marginBottom: "4px",
197
+ color: color.getStyle(),
198
+ textShadow: `0 0 4px ${color.getStyle()}`,
199
+ }}
200
+ >
201
+ {metric.label}
202
+ </div>
203
+ <div style={{ display: "flex", alignItems: "center", gap: "6px", marginBottom: "2px" }}>
204
+ <span style={{ fontWeight: 700, fontSize: "13px" }}>
205
+ {metric.value}
206
+ <span style={{ fontSize: "9px", opacity: 0.7, marginLeft: "2px" }}>
207
+ {metric.unit}
208
+ </span>
209
+ </span>
210
+ <span style={{ color: trendColor, fontSize: "10px" }}>
211
+ {trendArrow} {Math.abs(metric.trend).toFixed(1)}%
212
+ </span>
213
+ </div>
214
+ {metric.history && metric.history.length > 1 && (
215
+ <svg
216
+ width="96"
217
+ height="20"
218
+ viewBox="0 0 96 20"
219
+ style={{ display: "block", marginTop: "4px" }}
220
+ >
221
+ <polyline
222
+ points={metric.history
223
+ .map((v, i) => {
224
+ const min = Math.min(...metric.history!);
225
+ const max = Math.max(...metric.history!);
226
+ const range = max - min || 1;
227
+ const x = (i / (metric.history!.length - 1)) * 96;
228
+ const y = 18 - ((v - min) / range) * 16;
229
+ return `${x},${y}`;
230
+ })
231
+ .join(" ")}
232
+ fill="none"
233
+ stroke={color.getStyle()}
234
+ strokeWidth="1.5"
235
+ opacity="0.7"
236
+ />
237
+ </svg>
238
+ )}
239
+ {metric.category && (
240
+ <div style={{ fontSize: "8px", color: "rgba(100,116,139,1)", marginTop: "2px" }}>
241
+ {metric.category}
242
+ </div>
243
+ )}
244
+ </div>
245
+ </Html>
246
+ );
247
+ }
248
+
249
+ // --- Individual metric node ---
250
+
251
+ function MetricSphere({
252
+ metric,
253
+ position,
254
+ highlighted,
255
+ dimmed,
256
+ onMetricClick,
257
+ onMetricHover,
258
+ }: {
259
+ metric: MetricNode;
260
+ position: THREE.Vector3;
261
+ highlighted: boolean;
262
+ dimmed: boolean;
263
+ onMetricClick?: (id: string) => void;
264
+ onMetricHover?: (id: string | null) => void;
265
+ }) {
266
+ const meshRef = React.useRef<THREE.Mesh>(null);
267
+ const glowRef = React.useRef<THREE.Mesh>(null);
268
+ const [isHovered, setIsHovered] = React.useState(false);
269
+
270
+ const statusColor = React.useMemo(() => getStatusColor(metric), [metric]);
271
+ const catColor = React.useMemo(() => new THREE.Color(getCategoryColor(metric.category)), [metric.category]);
272
+ const baseScale = getNodeScale(metric);
273
+ const sphereRadius = 0.18;
274
+
275
+ const isCritical = metric.threshold ? metric.value >= metric.threshold.critical : false;
276
+ const isWarn = metric.threshold
277
+ ? metric.value >= metric.threshold.warn && !isCritical
278
+ : false;
279
+
280
+ useFrame((state) => {
281
+ if (!meshRef.current) return;
282
+ const t = state.clock.elapsedTime;
283
+
284
+ // Breathing pulse
285
+ const pulseSpeed = isCritical ? 4 : 2.5;
286
+ const pulseAmount = isCritical ? 0.15 : 0.06;
287
+ const pulse = 1 + Math.sin(t * pulseSpeed * (Math.PI / 1.25)) * pulseAmount;
288
+
289
+ const targetScale = (highlighted ? 1.4 : dimmed ? 0.7 : 1) * baseScale * pulse;
290
+ meshRef.current.scale.lerp(
291
+ new THREE.Vector3(targetScale, targetScale, targetScale),
292
+ 0.1
293
+ );
294
+
295
+ // Slow drift
296
+ meshRef.current.position.y =
297
+ position.y + Math.sin(t * 0.4 + position.x * 2) * 0.08;
298
+
299
+ // Glow ring
300
+ if (glowRef.current) {
301
+ const glowScale = 1.8 + Math.sin(t * pulseSpeed * (Math.PI / 1.25)) * 0.2;
302
+ glowRef.current.scale.setScalar(glowScale);
303
+ const mat = glowRef.current.material as THREE.MeshBasicMaterial;
304
+ if (mat) {
305
+ mat.opacity = (highlighted ? 0.3 : 0.12) + Math.sin(t * pulseSpeed) * 0.05;
306
+ }
307
+ }
308
+ });
309
+
310
+ const handlePointerEnter = React.useCallback(
311
+ (e: ThreeEvent<PointerEvent>) => {
312
+ e.stopPropagation();
313
+ setIsHovered(true);
314
+ onMetricHover?.(metric.id);
315
+ document.body.style.cursor = "pointer";
316
+ },
317
+ [metric.id, onMetricHover]
318
+ );
319
+
320
+ const handlePointerLeave = React.useCallback(() => {
321
+ setIsHovered(false);
322
+ onMetricHover?.(null);
323
+ document.body.style.cursor = "auto";
324
+ }, [onMetricHover]);
325
+
326
+ const handleClick = React.useCallback(
327
+ (e: ThreeEvent<PointerEvent>) => {
328
+ e.stopPropagation();
329
+ onMetricClick?.(metric.id);
330
+ },
331
+ [metric.id, onMetricClick]
332
+ );
333
+
334
+ return (
335
+ <group position={[position.x, position.y, position.z]}>
336
+ {/* Core sphere */}
337
+ <mesh
338
+ ref={meshRef}
339
+ onPointerEnter={handlePointerEnter}
340
+ onPointerLeave={handlePointerLeave}
341
+ onClick={handleClick}
342
+ >
343
+ <sphereGeometry args={[sphereRadius, 24, 24]} />
344
+ <meshStandardMaterial
345
+ color={statusColor}
346
+ emissive={statusColor}
347
+ emissiveIntensity={highlighted ? 1.2 : isCritical ? 0.9 : 0.5}
348
+ transparent
349
+ opacity={dimmed ? 0.4 : 0.9}
350
+ metalness={0.3}
351
+ roughness={0.4}
352
+ />
353
+ </mesh>
354
+
355
+ {/* Glow ring */}
356
+ <mesh ref={glowRef} scale={1.8}>
357
+ <sphereGeometry args={[sphereRadius, 16, 16]} />
358
+ <meshBasicMaterial
359
+ color={statusColor}
360
+ transparent
361
+ opacity={0.12}
362
+ />
363
+ </mesh>
364
+
365
+ {/* Pulsing ring for warn/critical */}
366
+ {(isWarn || isCritical) && (
367
+ <mesh rotation={[Math.PI / 2, 0, 0]}>
368
+ <ringGeometry args={[sphereRadius * 1.6, sphereRadius * 2.2, 32]} />
369
+ <meshBasicMaterial
370
+ color={statusColor}
371
+ transparent
372
+ opacity={0.15}
373
+ side={THREE.DoubleSide}
374
+ />
375
+ </mesh>
376
+ )}
377
+
378
+ {/* Sparkline halo */}
379
+ {metric.history && metric.history.length > 1 && (
380
+ <SparklineHalo
381
+ history={metric.history}
382
+ color={catColor}
383
+ radius={sphereRadius * 2.5}
384
+ />
385
+ )}
386
+
387
+ {/* Hover tooltip */}
388
+ {isHovered && <MetricTooltip metric={metric} color={statusColor} />}
389
+ </group>
390
+ );
391
+ }
392
+
393
+ // --- Connection lines ---
394
+
395
+ function ConnectionLine({
396
+ from,
397
+ to,
398
+ strength = 0.5,
399
+ }: {
400
+ from: THREE.Vector3;
401
+ to: THREE.Vector3;
402
+ strength?: number;
403
+ }) {
404
+ return (
405
+ <Line
406
+ points={[
407
+ [from.x, from.y, from.z],
408
+ [to.x, to.y, to.z],
409
+ ]}
410
+ color={NEON.cyan}
411
+ lineWidth={1}
412
+ transparent
413
+ opacity={0.08 + strength * 0.22}
414
+ />
415
+ );
416
+ }
417
+
418
+ /**
419
+ * MetricsGalaxy - A 3D galaxy visualization for system metrics monitoring.
420
+ *
421
+ * Renders metric nodes as pulsing spheres arranged in galaxy, grid, or radial layouts.
422
+ * Supports threshold-based coloring, sparkline halos, connection lines, and interactive tooltips.
423
+ */
424
+ export function MetricsGalaxy({
425
+ metrics,
426
+ connections,
427
+ layout = "galaxy",
428
+ onMetricClick,
429
+ onMetricHover,
430
+ highlightedId = null,
431
+ autoRotate = false,
432
+ }: MetricsGalaxyProps) {
433
+ const groupRef = React.useRef<THREE.Group>(null);
434
+
435
+ const positions = React.useMemo(
436
+ () => computeLayout(metrics, layout),
437
+ [metrics, layout]
438
+ );
439
+
440
+ // Auto-rotation
441
+ useFrame((_, delta) => {
442
+ if (autoRotate && groupRef.current) {
443
+ groupRef.current.rotation.y += delta * 0.08;
444
+ }
445
+ });
446
+
447
+ return (
448
+ <group ref={groupRef}>
449
+ {/* Connection lines (render first, behind nodes) */}
450
+ {connections?.map((conn) => {
451
+ const fromPos = positions.get(conn.from);
452
+ const toPos = positions.get(conn.to);
453
+ if (!fromPos || !toPos) return null;
454
+ return (
455
+ <ConnectionLine
456
+ key={`${conn.from}-${conn.to}`}
457
+ from={fromPos}
458
+ to={toPos}
459
+ strength={conn.strength}
460
+ />
461
+ );
462
+ })}
463
+
464
+ {/* Metric nodes */}
465
+ {metrics.map((metric) => {
466
+ const pos = positions.get(metric.id);
467
+ if (!pos) return null;
468
+ return (
469
+ <MetricSphere
470
+ key={metric.id}
471
+ metric={metric}
472
+ position={pos}
473
+ highlighted={highlightedId === metric.id}
474
+ dimmed={highlightedId != null && highlightedId !== metric.id}
475
+ onMetricClick={onMetricClick}
476
+ onMetricHover={onMetricHover}
477
+ />
478
+ );
479
+ })}
480
+ </group>
481
+ );
482
+ }
483
+
484
+ MetricsGalaxy.displayName = "MetricsGalaxy";
@@ -0,0 +1,6 @@
1
+ export { MetricsGalaxy } from "./MetricsGalaxy";
2
+ export type {
3
+ MetricNode,
4
+ MetricConnection,
5
+ MetricsGalaxyProps,
6
+ } from "./types";
@@ -0,0 +1,26 @@
1
+ export interface MetricNode {
2
+ id: string;
3
+ label: string;
4
+ value: number;
5
+ unit: string; // "%", "ms", "MB", "req/s"
6
+ trend: number; // positive = up, negative = down
7
+ threshold?: { warn: number; critical: number };
8
+ history?: number[]; // sparkline data (last N values)
9
+ category?: string; // for grouping/coloring
10
+ }
11
+
12
+ export interface MetricConnection {
13
+ from: string;
14
+ to: string;
15
+ strength?: number; // 0-1, affects line opacity
16
+ }
17
+
18
+ export interface MetricsGalaxyProps {
19
+ metrics: MetricNode[];
20
+ connections?: MetricConnection[];
21
+ layout?: "galaxy" | "grid" | "radial";
22
+ onMetricClick?: (id: string) => void;
23
+ onMetricHover?: (id: string | null) => void;
24
+ highlightedId?: string | null;
25
+ autoRotate?: boolean;
26
+ }
@@ -0,0 +1,184 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Canvas } from "@react-three/fiber";
3
+ import { OrbitControls, Stars } from "@react-three/drei";
4
+ import * as React from "react";
5
+ import { NetworkTopology } from "./NetworkTopology";
6
+ import type { NetworkEdge, NetworkNode } from "./types";
7
+
8
+ const demoNodes: NetworkNode[] = [
9
+ {
10
+ id: "fw-1",
11
+ type: "firewall",
12
+ hostname: "gateway-fw",
13
+ ip: "10.0.0.1",
14
+ status: "healthy",
15
+ services: ["vpn", "dpi"],
16
+ },
17
+ {
18
+ id: "router-1",
19
+ type: "router",
20
+ hostname: "core-router",
21
+ ip: "10.0.0.254",
22
+ status: "warning",
23
+ services: ["bgp", "ospf", "mpls"],
24
+ vulnerabilities: 2,
25
+ },
26
+ {
27
+ id: "srv-1",
28
+ type: "server",
29
+ hostname: "db-prod-01",
30
+ ip: "10.0.2.10",
31
+ status: "healthy",
32
+ services: ["postgres", "redis"],
33
+ },
34
+ {
35
+ id: "srv-2",
36
+ type: "server",
37
+ hostname: "api-prod-02",
38
+ ip: "10.0.2.20",
39
+ status: "compromised",
40
+ services: ["api", "grpc"],
41
+ vulnerabilities: 5,
42
+ },
43
+ {
44
+ id: "ws-1",
45
+ type: "workstation",
46
+ hostname: "analyst-07",
47
+ ip: "10.0.4.33",
48
+ status: "healthy",
49
+ services: ["edr"],
50
+ },
51
+ {
52
+ id: "iot-1",
53
+ type: "iot",
54
+ hostname: "badge-reader",
55
+ ip: "10.0.6.15",
56
+ status: "warning",
57
+ services: ["ble"],
58
+ },
59
+ {
60
+ id: "cloud-1",
61
+ type: "cloud",
62
+ hostname: "aws-edge",
63
+ ip: "172.20.0.1",
64
+ status: "healthy",
65
+ services: ["cdn", "waf"],
66
+ },
67
+ ];
68
+
69
+ const demoEdges: NetworkEdge[] = [
70
+ {
71
+ id: "e1",
72
+ source: "fw-1",
73
+ target: "router-1",
74
+ protocol: "tcp",
75
+ encrypted: true,
76
+ status: "active",
77
+ bandwidth: 5800,
78
+ },
79
+ {
80
+ id: "e2",
81
+ source: "router-1",
82
+ target: "srv-1",
83
+ protocol: "https",
84
+ encrypted: true,
85
+ status: "active",
86
+ bandwidth: 3200,
87
+ },
88
+ {
89
+ id: "e3",
90
+ source: "router-1",
91
+ target: "srv-2",
92
+ protocol: "ssh",
93
+ encrypted: false,
94
+ status: "suspicious",
95
+ bandwidth: 1200,
96
+ },
97
+ {
98
+ id: "e4",
99
+ source: "router-1",
100
+ target: "ws-1",
101
+ protocol: "rdp",
102
+ encrypted: true,
103
+ status: "idle",
104
+ },
105
+ {
106
+ id: "e5",
107
+ source: "srv-2",
108
+ target: "iot-1",
109
+ protocol: "udp",
110
+ encrypted: false,
111
+ status: "blocked",
112
+ },
113
+ {
114
+ id: "e6",
115
+ source: "fw-1",
116
+ target: "cloud-1",
117
+ protocol: "https",
118
+ encrypted: true,
119
+ status: "active",
120
+ bandwidth: 6100,
121
+ },
122
+ ];
123
+
124
+ const meta: Meta<typeof NetworkTopology> = {
125
+ title: "Primitives/3D/Security/NetworkTopology",
126
+ component: NetworkTopology,
127
+ parameters: {
128
+ layout: "fullscreen",
129
+ backgrounds: { default: "dark" },
130
+ },
131
+ decorators: [
132
+ (Story) => (
133
+ <div style={{ width: "100vw", height: "100vh", background: "#050812" }}>
134
+ <Canvas camera={{ position: [0, 4, 8], fov: 45 }} dpr={[1, 2]}>
135
+ <color attach="background" args={["#050812"]} />
136
+ <fog attach="fog" args={["#050812", 8, 20]} />
137
+ <Stars radius={60} depth={30} count={1200} factor={2} saturation={0} fade speed={0.4} />
138
+ <ambientLight intensity={0.3} />
139
+ <pointLight position={[6, 8, 6]} intensity={0.7} color="#3b82f6" />
140
+ <pointLight position={[-6, 4, -6]} intensity={0.4} color="#22d3ee" />
141
+ <OrbitControls enableDamping dampingFactor={0.06} />
142
+ <Story />
143
+ </Canvas>
144
+ </div>
145
+ ),
146
+ ],
147
+ };
148
+
149
+ export default meta;
150
+ type Story = StoryObj<typeof NetworkTopology>;
151
+
152
+ export const ForceLayout: Story = {
153
+ args: {
154
+ nodes: demoNodes,
155
+ edges: demoEdges,
156
+ layout: "force",
157
+ showTraffic: true,
158
+ showLabels: true,
159
+ highlightPath: ["fw-1", "router-1", "srv-2"],
160
+ },
161
+ };
162
+
163
+ export const Hierarchical: Story = {
164
+ args: {
165
+ nodes: demoNodes,
166
+ edges: demoEdges,
167
+ layout: "hierarchical",
168
+ showTraffic: true,
169
+ showLabels: true,
170
+ highlightPath: ["fw-1", "router-1", "srv-1"],
171
+ theme: "blueprint",
172
+ },
173
+ };
174
+
175
+ export const Radial: Story = {
176
+ args: {
177
+ nodes: demoNodes,
178
+ edges: demoEdges,
179
+ layout: "radial",
180
+ showTraffic: false,
181
+ showLabels: true,
182
+ theme: "matrix",
183
+ },
184
+ };