@omiron33/omi-neuron-web 0.1.2 → 0.1.6

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.
@@ -1,7 +1,7 @@
1
1
  import { useRef, useState, useMemo, useEffect } from 'react';
2
2
  import * as THREE from 'three';
3
3
  import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
4
- import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer.js';
4
+ import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js';
5
5
  import { jsx, jsxs } from 'react/jsx-runtime';
6
6
 
7
7
  // src/visualization/constants.ts
@@ -14,7 +14,7 @@ var DEFAULT_THEME = {
14
14
  edgeActive: "#c6d4ff",
15
15
  edgeSelected: "#ffffff",
16
16
  labelText: "#ffffff",
17
- labelBackground: "rgba(0, 0, 0, 0.8)"
17
+ labelBackground: "rgba(5, 6, 31, 0.8)"
18
18
  },
19
19
  typography: {
20
20
  labelFontFamily: "system-ui, sans-serif",
@@ -33,8 +33,8 @@ var DEFAULT_THEME = {
33
33
  edgeFlowSpeed: 1.2,
34
34
  fogEnabled: true,
35
35
  fogColor: "#020314",
36
- fogNear: 32,
37
- fogFar: 180
36
+ fogNear: 24,
37
+ fogFar: 160
38
38
  },
39
39
  animation: {
40
40
  focusDuration: 800,
@@ -52,8 +52,8 @@ var SceneManager = class {
52
52
  this.container = container;
53
53
  this.config = config;
54
54
  this.scene = new THREE.Scene();
55
- this.camera = new THREE.PerspectiveCamera(60, 1, 0.1, 2e3);
56
- this.renderer = new THREE.WebGLRenderer({ antialias: true });
55
+ this.camera = new THREE.PerspectiveCamera(config.cameraFov ?? 52, 1, 0.1, 220);
56
+ this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
57
57
  this.labelRenderer = new CSS2DRenderer();
58
58
  this.controls = new OrbitControls(this.camera, this.renderer.domElement);
59
59
  }
@@ -70,6 +70,7 @@ var SceneManager = class {
70
70
  ambientLight = null;
71
71
  keyLight = null;
72
72
  fillLight = null;
73
+ resizeObserver = null;
73
74
  initialize() {
74
75
  const { cameraPosition, cameraTarget, backgroundColor } = this.config;
75
76
  this.scene.background = new THREE.Color(backgroundColor);
@@ -84,9 +85,14 @@ var SceneManager = class {
84
85
  this.controls.update();
85
86
  this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, this.config.pixelRatioCap));
86
87
  this.renderer.outputColorSpace = THREE.SRGBColorSpace;
87
- this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
88
- this.renderer.toneMappingExposure = 1.05;
88
+ this.renderer.toneMapping = THREE.NoToneMapping;
89
+ this.renderer.toneMappingExposure = 1;
89
90
  this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
91
+ this.renderer.domElement.style.position = "absolute";
92
+ this.renderer.domElement.style.top = "0";
93
+ this.renderer.domElement.style.left = "0";
94
+ this.renderer.domElement.style.width = "100%";
95
+ this.renderer.domElement.style.height = "100%";
90
96
  this.labelRenderer.setSize(this.container.clientWidth, this.container.clientHeight);
91
97
  this.labelRenderer.domElement.style.position = "absolute";
92
98
  this.labelRenderer.domElement.style.top = "0";
@@ -101,11 +107,17 @@ var SceneManager = class {
101
107
  this.initStarfield();
102
108
  }
103
109
  window.addEventListener("resize", this.resize);
110
+ if (typeof ResizeObserver !== "undefined") {
111
+ this.resizeObserver = new ResizeObserver(() => this.resize());
112
+ this.resizeObserver.observe(this.container);
113
+ }
104
114
  this.startAnimationLoop();
105
115
  }
106
116
  dispose() {
107
117
  this.stopAnimationLoop();
108
118
  window.removeEventListener("resize", this.resize);
119
+ this.resizeObserver?.disconnect();
120
+ this.resizeObserver = null;
109
121
  this.renderer.dispose();
110
122
  this.scene.clear();
111
123
  this.container.innerHTML = "";
@@ -143,6 +155,7 @@ var SceneManager = class {
143
155
  resize = () => {
144
156
  const width = this.container.clientWidth;
145
157
  const height = this.container.clientHeight;
158
+ if (!width || !height) return;
146
159
  this.camera.aspect = width / height;
147
160
  this.camera.updateProjectionMatrix();
148
161
  this.renderer.setSize(width, height);
@@ -197,20 +210,17 @@ var SceneManager = class {
197
210
  const geometry = new THREE.BufferGeometry();
198
211
  const positions = new Float32Array(count * 3);
199
212
  for (let i = 0; i < count; i += 1) {
200
- const radius = 180 + Math.random() * 120;
201
- const theta = Math.random() * Math.PI * 2;
202
- const phi = Math.acos(2 * Math.random() - 1);
203
- positions[i * 3] = radius * Math.sin(phi) * Math.cos(theta);
204
- positions[i * 3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
205
- positions[i * 3 + 2] = radius * Math.cos(phi);
213
+ positions[i * 3] = THREE.MathUtils.randFloatSpread(70);
214
+ positions[i * 3 + 1] = THREE.MathUtils.randFloatSpread(70);
215
+ positions[i * 3 + 2] = THREE.MathUtils.randFloatSpread(70);
206
216
  }
207
217
  geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
208
218
  const material = new THREE.PointsMaterial({
209
219
  color: this.config.starfieldColor,
210
- size: 0.6,
220
+ size: 0.22,
211
221
  sizeAttenuation: true,
212
222
  transparent: true,
213
- opacity: 0.45,
223
+ opacity: 0.5,
214
224
  depthWrite: false
215
225
  });
216
226
  this.starfield = new THREE.Points(geometry, material);
@@ -251,38 +261,53 @@ var NodeRenderer = class {
251
261
  this.scene = scene;
252
262
  this.config = config;
253
263
  this.scene.add(this.group);
264
+ this.glowTexture = this.createGlowTexture();
265
+ if (config.labelOffset) {
266
+ this.labelOffset.set(...config.labelOffset);
267
+ }
254
268
  }
255
269
  group = new THREE.Group();
256
270
  nodeStates = /* @__PURE__ */ new Map();
257
271
  hoveredNodeId = null;
258
272
  selectedNodeId = null;
273
+ glowTexture = null;
274
+ labelOffset = new THREE.Vector3(0, 0.65, 0);
259
275
  renderNodes(nodes) {
260
276
  this.clear();
277
+ const shouldRenderLabels = this.config.maxVisibleLabels > 0 && this.config.labelDistance > 0;
261
278
  nodes.forEach((node) => {
262
279
  const color = new THREE.Color(
263
280
  this.config.domainColors[node.domain] ?? this.config.defaultColor
264
281
  );
265
- const geometry = new THREE.SphereGeometry(this.config.baseScale, 18, 18);
266
- const material = new THREE.MeshStandardMaterial({
282
+ const material = new THREE.SpriteMaterial({
283
+ map: this.glowTexture ?? void 0,
267
284
  color,
268
- roughness: 0.45,
269
- metalness: 0.1,
270
- emissive: color.clone().multiplyScalar(this.config.glowIntensity * 0.4),
271
- emissiveIntensity: 1
285
+ transparent: true,
286
+ opacity: 0.78,
287
+ depthWrite: false
272
288
  });
273
- const mesh = new THREE.Mesh(geometry, material);
289
+ const sprite = new THREE.Sprite(material);
274
290
  const position = new THREE.Vector3();
275
291
  if (node.position) {
276
292
  position.set(...node.position);
277
293
  }
278
- mesh.position.copy(position);
294
+ sprite.position.copy(position);
279
295
  const tierScale = node.tier ? this.config.tierScales[node.tier] ?? 1 : 1;
280
296
  const baseScale = this.config.baseScale * tierScale;
281
- mesh.scale.setScalar(baseScale);
282
- mesh.userData = { nodeId: node.id, nodeSlug: node.slug };
283
- this.group.add(mesh);
297
+ sprite.scale.setScalar(baseScale);
298
+ sprite.userData = { nodeId: node.id, nodeSlug: node.slug };
299
+ this.group.add(sprite);
300
+ let labelObject = null;
301
+ if (shouldRenderLabels) {
302
+ const labelElement = this.createLabelElement(node, color);
303
+ labelObject = new CSS2DObject(labelElement);
304
+ labelObject.position.copy(sprite.position).add(this.labelOffset);
305
+ this.scene.add(labelObject);
306
+ }
284
307
  this.nodeStates.set(node.id, {
285
- mesh,
308
+ sprite,
309
+ material,
310
+ label: labelObject,
286
311
  basePosition: position.clone(),
287
312
  baseScale,
288
313
  phase: Math.random() * Math.PI * 2,
@@ -298,7 +323,7 @@ var NodeRenderer = class {
298
323
  if (!state) return;
299
324
  if (updates.position) {
300
325
  state.basePosition.set(...updates.position);
301
- state.mesh.position.set(...updates.position);
326
+ state.sprite.position.set(...updates.position);
302
327
  }
303
328
  if (updates.tier) {
304
329
  const tierScale = this.config.tierScales[updates.tier] ?? 1;
@@ -309,18 +334,24 @@ var NodeRenderer = class {
309
334
  this.config.domainColors[updates.domain] ?? this.config.defaultColor
310
335
  );
311
336
  state.baseColor = color;
312
- const material = state.mesh.material;
313
- material.color = color;
314
- material.emissive = color.clone().multiplyScalar(this.config.glowIntensity * 0.4);
337
+ state.material.color = color;
315
338
  }
316
339
  }
317
340
  removeNode(nodeId) {
318
341
  const state = this.nodeStates.get(nodeId);
319
342
  if (!state) return;
320
- this.group.remove(state.mesh);
343
+ if (state.label) {
344
+ this.scene.remove(state.label);
345
+ }
346
+ this.group.remove(state.sprite);
321
347
  this.nodeStates.delete(nodeId);
322
348
  }
323
349
  clear() {
350
+ this.nodeStates.forEach((state) => {
351
+ if (state.label) {
352
+ this.scene.remove(state.label);
353
+ }
354
+ });
324
355
  this.group.clear();
325
356
  this.nodeStates.clear();
326
357
  this.hoveredNodeId = null;
@@ -328,29 +359,130 @@ var NodeRenderer = class {
328
359
  }
329
360
  showNodes(nodeIds) {
330
361
  nodeIds.forEach((id) => {
331
- const obj = this.nodeObjects.get(id);
332
- if (obj) obj.visible = true;
362
+ const state = this.nodeStates.get(id);
363
+ if (state) state.sprite.visible = true;
333
364
  });
334
365
  }
335
366
  hideNodes(nodeIds) {
336
367
  nodeIds.forEach((id) => {
337
- const obj = this.nodeObjects.get(id);
338
- if (obj) obj.visible = false;
368
+ const state = this.nodeStates.get(id);
369
+ if (state) state.sprite.visible = false;
339
370
  });
340
371
  }
341
372
  setVisibleNodes(nodeIds) {
342
373
  if (!nodeIds) {
343
374
  this.nodeStates.forEach((state) => {
344
- state.mesh.visible = true;
375
+ state.sprite.visible = true;
345
376
  });
346
377
  return;
347
378
  }
348
379
  const visibleSet = new Set(nodeIds);
349
380
  this.nodeStates.forEach((state, id) => {
350
- state.mesh.visible = visibleSet.has(id);
381
+ state.sprite.visible = visibleSet.has(id);
351
382
  });
352
383
  }
353
- updateLabelVisibility() {
384
+ updateLabelVisibility(camera) {
385
+ if (this.config.maxVisibleLabels <= 0 || this.config.labelDistance <= 0) {
386
+ this.nodeStates.forEach((state) => {
387
+ if (state.label) state.label.visible = false;
388
+ });
389
+ return;
390
+ }
391
+ const entries = [];
392
+ this.nodeStates.forEach((state) => {
393
+ if (!state.label) return;
394
+ if (!state.sprite.visible) {
395
+ state.label.visible = false;
396
+ return;
397
+ }
398
+ const distance = camera.position.distanceTo(state.sprite.position);
399
+ entries.push({ state, distance });
400
+ });
401
+ entries.sort((a, b) => a.distance - b.distance);
402
+ entries.forEach((entry, index) => {
403
+ const visible = entry.distance <= this.config.labelDistance && index < this.config.maxVisibleLabels;
404
+ entry.state.label.visible = visible;
405
+ });
406
+ }
407
+ createLabelElement(node, accent) {
408
+ const wrapper = document.createElement("div");
409
+ wrapper.style.borderRadius = "10px";
410
+ wrapper.style.border = "1px solid rgba(255, 255, 255, 0.12)";
411
+ wrapper.style.background = this.config.labelBackground;
412
+ wrapper.style.color = this.config.labelTextColor;
413
+ wrapper.style.fontFamily = this.config.labelFontFamily;
414
+ wrapper.style.fontSize = `${this.config.labelFontSize}px`;
415
+ wrapper.style.fontWeight = this.config.labelFontWeight;
416
+ wrapper.style.padding = "6px 8px";
417
+ wrapper.style.boxShadow = "0 10px 30px rgba(5, 10, 20, 0.35)";
418
+ wrapper.style.backdropFilter = "blur(10px)";
419
+ wrapper.style.pointerEvents = "none";
420
+ wrapper.style.maxWidth = "220px";
421
+ const isInsight = node.tier === "insight" || node.domain === "insight";
422
+ if (isInsight) {
423
+ const accentBright = accent.clone().lerp(new THREE.Color("#ffffff"), 0.35);
424
+ wrapper.style.border = `1px solid ${this.toRgba(accentBright, 0.6)}`;
425
+ wrapper.style.background = this.toRgba(accent, 0.2);
426
+ wrapper.style.color = this.toRgba(accentBright, 0.95);
427
+ }
428
+ const badgeRow = document.createElement("div");
429
+ badgeRow.style.display = "flex";
430
+ badgeRow.style.flexWrap = "wrap";
431
+ badgeRow.style.gap = "4px";
432
+ badgeRow.style.marginBottom = "4px";
433
+ const makeBadge = (text, tone) => {
434
+ const badge = document.createElement("span");
435
+ badge.textContent = text;
436
+ badge.style.display = "inline-flex";
437
+ badge.style.alignItems = "center";
438
+ badge.style.gap = "4px";
439
+ badge.style.borderRadius = "999px";
440
+ badge.style.padding = "2px 6px";
441
+ badge.style.fontSize = "0.6rem";
442
+ badge.style.fontWeight = "600";
443
+ badge.style.textTransform = "uppercase";
444
+ badge.style.letterSpacing = "0.2em";
445
+ if (tone === "accent") {
446
+ badge.style.border = `1px solid ${this.toRgba(accent, 0.6)}`;
447
+ badge.style.background = this.toRgba(accent, 0.4);
448
+ badge.style.color = "#f5f7ff";
449
+ } else {
450
+ badge.style.border = "1px solid rgba(255, 255, 255, 0.2)";
451
+ badge.style.background = "rgba(255, 255, 255, 0.08)";
452
+ badge.style.color = "rgba(255, 255, 255, 0.8)";
453
+ }
454
+ return badge;
455
+ };
456
+ if (isInsight) {
457
+ badgeRow.appendChild(makeBadge("Insight", "accent"));
458
+ const statusRaw = node.metadata?.status ?? node.metadata?.draftNodeStatus ?? node.metadata?.studyPathStatus;
459
+ if (statusRaw) {
460
+ const formatted = statusRaw.replace(/[_-]/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
461
+ badgeRow.appendChild(makeBadge(formatted, "muted"));
462
+ }
463
+ }
464
+ if (badgeRow.childElementCount > 0) {
465
+ wrapper.appendChild(badgeRow);
466
+ }
467
+ const title = document.createElement("div");
468
+ title.textContent = node.label;
469
+ title.style.fontSize = "0.8rem";
470
+ title.style.fontWeight = "600";
471
+ wrapper.appendChild(title);
472
+ if (node.ref) {
473
+ const reference = document.createElement("div");
474
+ reference.textContent = node.ref;
475
+ reference.style.fontSize = "0.65rem";
476
+ reference.style.opacity = "0.7";
477
+ wrapper.appendChild(reference);
478
+ }
479
+ return wrapper;
480
+ }
481
+ toRgba(color, alpha) {
482
+ const r = Math.round(color.r * 255);
483
+ const g = Math.round(color.g * 255);
484
+ const b = Math.round(color.b * 255);
485
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
354
486
  }
355
487
  highlightNode(nodeId) {
356
488
  this.setHoveredNode(nodeId);
@@ -365,14 +497,14 @@ var NodeRenderer = class {
365
497
  }
366
498
  getNodePosition(nodeId) {
367
499
  const state = this.nodeStates.get(nodeId);
368
- return state ? state.mesh.position.clone() : null;
500
+ return state ? state.sprite.position.clone() : null;
369
501
  }
370
502
  getNodeObject(nodeId) {
371
503
  const state = this.nodeStates.get(nodeId);
372
- return state?.mesh ?? null;
504
+ return state?.sprite ?? null;
373
505
  }
374
506
  getNodeObjects() {
375
- return Array.from(this.nodeStates.values()).map((state) => state.mesh);
507
+ return Array.from(this.nodeStates.values()).map((state) => state.sprite);
376
508
  }
377
509
  setHoveredNode(nodeId) {
378
510
  if (this.hoveredNodeId === nodeId) return;
@@ -410,13 +542,16 @@ var NodeRenderer = class {
410
542
  const drift = Math.sin(elapsed * this.config.ambientMotionSpeed + state.phase) * this.config.ambientMotionAmplitude;
411
543
  const driftX = Math.cos(elapsed * this.config.ambientMotionSpeed * 0.6 + state.phase) * this.config.ambientMotionAmplitude * 0.45;
412
544
  const driftZ = Math.sin(elapsed * this.config.ambientMotionSpeed * 0.4 + state.phase) * this.config.ambientMotionAmplitude * 0.35;
413
- state.mesh.position.set(
545
+ state.sprite.position.set(
414
546
  state.basePosition.x + driftX,
415
547
  state.basePosition.y + drift,
416
548
  state.basePosition.z + driftZ
417
549
  );
418
550
  } else {
419
- state.mesh.position.copy(state.basePosition);
551
+ state.sprite.position.copy(state.basePosition);
552
+ }
553
+ if (state.label) {
554
+ state.label.position.copy(state.sprite.position).add(this.labelOffset);
420
555
  }
421
556
  const hoverScale = state.hovered ? this.config.hoverScale : 1;
422
557
  const selectedScale = state.selected ? this.config.selectedScale : 1;
@@ -430,17 +565,42 @@ var NodeRenderer = class {
430
565
  }
431
566
  }
432
567
  const targetScale = state.baseScale * hoverScale * selectedScale * (1 + pulseScale);
433
- const currentScale = state.mesh.scale.x;
568
+ const currentScale = state.sprite.scale.x;
434
569
  const nextScale = currentScale + (targetScale - currentScale) * 0.18;
435
- state.mesh.scale.setScalar(nextScale);
436
- const material = state.mesh.material;
437
- const emissiveBoost = state.selected ? 0.65 : state.hovered ? 0.45 : 0.25;
438
- material.emissive = state.baseColor.clone().multiplyScalar(this.config.glowIntensity * emissiveBoost);
570
+ state.sprite.scale.setScalar(nextScale);
571
+ const material = state.material;
572
+ const baseOpacity = 0.78;
573
+ const hoverOpacity = Math.min(0.95, baseOpacity + 0.12);
574
+ const selectedOpacity = 1;
575
+ material.opacity = state.selected ? selectedOpacity : state.hovered ? hoverOpacity : baseOpacity;
576
+ material.color.copy(state.baseColor);
577
+ if (state.selected) {
578
+ material.color.lerp(new THREE.Color("#ffffff"), 0.25);
579
+ }
439
580
  });
440
581
  }
441
582
  dispose() {
442
583
  this.clear();
443
584
  this.scene.remove(this.group);
585
+ this.glowTexture?.dispose();
586
+ this.glowTexture = null;
587
+ }
588
+ createGlowTexture() {
589
+ const size = 256;
590
+ const canvas = document.createElement("canvas");
591
+ canvas.width = size;
592
+ canvas.height = size;
593
+ const ctx = canvas.getContext("2d");
594
+ if (!ctx) return null;
595
+ const gradient = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
596
+ gradient.addColorStop(0, "rgba(255,255,255,0.95)");
597
+ gradient.addColorStop(0.4, "rgba(255,255,255,0.45)");
598
+ gradient.addColorStop(1, "rgba(255,255,255,0)");
599
+ ctx.fillStyle = gradient;
600
+ ctx.fillRect(0, 0, size, size);
601
+ const texture = new THREE.CanvasTexture(canvas);
602
+ texture.colorSpace = THREE.SRGBColorSpace;
603
+ return texture;
444
604
  }
445
605
  };
446
606
  var EdgeRenderer = class {
@@ -564,6 +724,35 @@ var EdgeRenderer = class {
564
724
 
565
725
  // src/visualization/layouts/fuzzy-layout.ts
566
726
  var GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5));
727
+ var ATLAS_POSITION_OVERRIDES = {
728
+ uap: [6, 2, 2],
729
+ ez1: [2, 4, 0],
730
+ neph: [-3, 3, 2],
731
+ jude6: [-1, 1.5, 2.5],
732
+ "2p24": [1.2, 0.3, 2.6],
733
+ aiimg: [4.6, -1.6, 1.8],
734
+ rev13: [2.7, -2.8, 0.6],
735
+ xhuman: [0.4, -3.4, -1.8],
736
+ dan243: [-1.6, -2.1, -2.9],
737
+ llm: [3.4, 0.3, -2.4],
738
+ babel: [0.9, 1.4, -3.4],
739
+ warfare: [-4.6, 0.4, -1.5],
740
+ eph612: [-5.5, -1.7, 0.5],
741
+ berea: [-0.2, 4.6, -1.3],
742
+ pharm: [-3.1, -2.4, 1.2],
743
+ testsp: [5.4, 3.2, 1.6]
744
+ };
745
+ function generateSpherePosition(index, total, radius) {
746
+ if (total <= 1) {
747
+ return [0, 0, 0];
748
+ }
749
+ const offset = 2 / total;
750
+ const increment = Math.PI * (3 - Math.sqrt(5));
751
+ const y = 1 - index * offset;
752
+ const r = Math.sqrt(Math.max(0, 1 - y * y));
753
+ const phi = index * increment;
754
+ return [Math.cos(phi) * r * radius, y * radius, Math.sin(phi) * r * radius];
755
+ }
567
756
  function hashString(input) {
568
757
  let hash = 2166136261;
569
758
  for (let i = 0; i < input.length; i += 1) {
@@ -585,17 +774,53 @@ function buildSeed(baseSeed, nodeKey) {
585
774
  return mulberry32(hashString(`${baseSeed}:${nodeKey}`));
586
775
  }
587
776
  function applyFuzzyLayout(nodes, options = {}) {
588
- const mode = options.mode ?? "auto";
777
+ const mode = options.mode ?? "atlas";
589
778
  if (mode === "positioned") {
590
779
  return nodes;
591
780
  }
592
- const needsLayout = mode === "fuzzy" || nodes.some((node) => !node.position);
593
- if (!needsLayout) {
781
+ const needsLayout = nodes.some((node) => !node.position);
782
+ if (mode === "auto" && !needsLayout) {
594
783
  return nodes;
595
784
  }
785
+ const spread = options.spread ?? 1;
786
+ const overrides = { ...ATLAS_POSITION_OVERRIDES, ...options.overrides ?? {} };
787
+ if (mode === "atlas" || mode === "auto") {
788
+ const baseRadius2 = (options.radius ?? 12) * spread;
789
+ const insightRadius = (options.insightRadius ?? Math.max(5, baseRadius2 * 0.4)) * spread;
790
+ const canonicalNodes = nodes.filter(
791
+ (node) => node.tier !== "insight" && node.domain !== "insight"
792
+ );
793
+ const insightNodes = nodes.filter(
794
+ (node) => node.tier === "insight" || node.domain === "insight"
795
+ );
796
+ const canonicalPositions = /* @__PURE__ */ new Map();
797
+ const insightPositions = /* @__PURE__ */ new Map();
798
+ canonicalNodes.forEach((node, index) => {
799
+ canonicalPositions.set(
800
+ node.id,
801
+ generateSpherePosition(index, canonicalNodes.length, baseRadius2)
802
+ );
803
+ });
804
+ insightNodes.forEach((node, index) => {
805
+ insightPositions.set(
806
+ node.id,
807
+ generateSpherePosition(index, insightNodes.length || 1, insightRadius)
808
+ );
809
+ });
810
+ return nodes.map((node) => {
811
+ const override = overrides[node.id] ?? overrides[node.slug];
812
+ if (node.position && !override) {
813
+ return node;
814
+ }
815
+ if (override) {
816
+ return { ...node, position: [...override] };
817
+ }
818
+ const fallback = (node.tier === "insight" || node.domain === "insight" ? insightPositions.get(node.id) : canonicalPositions.get(node.id)) ?? [0, 0, 0];
819
+ return { ...node, position: fallback };
820
+ });
821
+ }
596
822
  const baseSeed = options.seed ?? "omi-neuron-web";
597
823
  const count = Math.max(nodes.length, 1);
598
- const spread = options.spread ?? 1.2;
599
824
  const baseRadius = (options.radius ?? Math.max(4, Math.sqrt(count) * 2.4)) * spread;
600
825
  const jitter = (options.jitter ?? baseRadius * 0.12) * spread;
601
826
  const zSpread = (options.zSpread ?? baseRadius * 0.6) * spread;
@@ -798,6 +1023,8 @@ function NeuronWeb({
798
1023
  graphData,
799
1024
  className,
800
1025
  style,
1026
+ fullHeight,
1027
+ isFullScreen,
801
1028
  isLoading,
802
1029
  error,
803
1030
  renderEmptyState,
@@ -837,17 +1064,18 @@ function NeuronWeb({
837
1064
  }, [performanceMode, graphData.nodes.length]);
838
1065
  const sceneManager = useSceneManager(containerRef, {
839
1066
  backgroundColor: resolvedTheme.colors.background,
1067
+ cameraFov: 52,
840
1068
  cameraPosition: [4, 8, 20],
841
1069
  cameraTarget: [0, 0, 0],
842
1070
  minZoom: 4,
843
1071
  maxZoom: 42,
844
1072
  enableStarfield: resolvedTheme.effects.starfieldEnabled,
845
- starfieldCount: 1200,
1073
+ starfieldCount: resolvedPerformanceMode === "normal" ? 1200 : 700,
846
1074
  starfieldColor: resolvedTheme.effects.starfieldColor,
847
1075
  pixelRatioCap: 2,
848
- ambientLightIntensity: 0.7,
849
- keyLightIntensity: 1.1,
850
- fillLightIntensity: 0.6,
1076
+ ambientLightIntensity: 0.9,
1077
+ keyLightIntensity: 0.6,
1078
+ fillLightIntensity: 0.4,
851
1079
  fogEnabled: resolvedTheme.effects.fogEnabled,
852
1080
  fogColor: resolvedTheme.effects.fogColor,
853
1081
  fogNear: resolvedTheme.effects.fogNear,
@@ -858,16 +1086,22 @@ function NeuronWeb({
858
1086
  return new NodeRenderer(sceneManager.scene, {
859
1087
  domainColors: resolvedTheme.colors.domainColors,
860
1088
  defaultColor: resolvedTheme.colors.defaultDomainColor,
861
- baseScale: 0.4,
1089
+ baseScale: 1.15,
862
1090
  tierScales: {
863
- primary: 1.6,
864
- secondary: 1.2,
865
- tertiary: 1,
866
- insight: 1
1091
+ primary: 1.25,
1092
+ secondary: 1.1,
1093
+ tertiary: 0.95,
1094
+ insight: 1.05
867
1095
  },
868
1096
  glowIntensity: resolvedTheme.effects.glowEnabled ? resolvedTheme.effects.glowIntensity : 0,
869
- labelDistance: 20,
870
- maxVisibleLabels: 50,
1097
+ labelDistance: resolvedPerformanceMode === "normal" ? 26 : 0,
1098
+ maxVisibleLabels: resolvedPerformanceMode === "normal" ? 80 : 0,
1099
+ labelOffset: [0, 0.65, 0],
1100
+ labelFontFamily: resolvedTheme.typography.labelFontFamily,
1101
+ labelFontSize: resolvedTheme.typography.labelFontSize,
1102
+ labelFontWeight: resolvedTheme.typography.labelFontWeight,
1103
+ labelTextColor: resolvedTheme.colors.labelText,
1104
+ labelBackground: resolvedTheme.colors.labelBackground,
871
1105
  ambientMotionEnabled: resolvedTheme.effects.ambientMotionEnabled && resolvedPerformanceMode === "normal",
872
1106
  ambientMotionAmplitude: resolvedTheme.effects.ambientMotionAmplitude,
873
1107
  ambientMotionSpeed: resolvedTheme.effects.ambientMotionSpeed,
@@ -883,7 +1117,7 @@ function NeuronWeb({
883
1117
  defaultColor: resolvedTheme.colors.edgeDefault,
884
1118
  activeColor: resolvedTheme.colors.edgeActive,
885
1119
  selectedColor: resolvedTheme.colors.edgeSelected,
886
- baseOpacity: 0.5,
1120
+ baseOpacity: 0.45,
887
1121
  strengthOpacityScale: true,
888
1122
  edgeFlowEnabled: resolvedTheme.effects.edgeFlowEnabled && resolvedPerformanceMode === "normal",
889
1123
  edgeFlowSpeed: resolvedTheme.effects.edgeFlowSpeed
@@ -964,6 +1198,7 @@ function NeuronWeb({
964
1198
  if (!sceneManager || !nodeRenderer || !edgeRenderer) return;
965
1199
  return sceneManager.addFrameListener((delta, elapsed) => {
966
1200
  nodeRenderer.update(delta, elapsed);
1201
+ nodeRenderer.updateLabelVisibility(sceneManager.camera);
967
1202
  edgeRenderer.update(delta, elapsed);
968
1203
  animationController?.update();
969
1204
  if (hoverCardRef.current && hoveredNodeId) {
@@ -1084,14 +1319,30 @@ function NeuronWeb({
1084
1319
  if (!resolvedNodes.length) {
1085
1320
  return /* @__PURE__ */ jsx("div", { className, style, "aria-label": ariaLabel, children: renderEmptyState ? renderEmptyState() : /* @__PURE__ */ jsx("div", { children: "No data" }) });
1086
1321
  }
1322
+ const resolvedStyle = {
1323
+ position: isFullScreen ? "fixed" : "relative",
1324
+ inset: isFullScreen ? 0 : void 0,
1325
+ width: isFullScreen ? "100vw" : "100%",
1326
+ height: isFullScreen ? "100vh" : "100%",
1327
+ minHeight: !isFullScreen && fullHeight ? "100vh" : void 0,
1328
+ overflow: "hidden",
1329
+ background: resolvedTheme.colors.background,
1330
+ ...style
1331
+ };
1087
1332
  return /* @__PURE__ */ jsxs(
1088
1333
  "div",
1089
1334
  {
1090
1335
  className,
1091
- style: { position: "relative", width: "100%", height: "100%", overflow: "hidden", ...style },
1336
+ style: resolvedStyle,
1092
1337
  "aria-label": ariaLabel,
1093
1338
  children: [
1094
- /* @__PURE__ */ jsx("div", { ref: containerRef, style: { width: "100%", height: "100%" } }),
1339
+ /* @__PURE__ */ jsx(
1340
+ "div",
1341
+ {
1342
+ ref: containerRef,
1343
+ style: { position: "absolute", inset: 0, width: "100%", height: "100%" }
1344
+ }
1345
+ ),
1095
1346
  hoverCardEnabled && hoveredNode && /* @__PURE__ */ jsx(
1096
1347
  "div",
1097
1348
  {
@@ -1190,5 +1441,5 @@ var ThemeEngine = class {
1190
1441
  };
1191
1442
 
1192
1443
  export { DEFAULT_THEME, NeuronWeb, SceneManager, ThemeEngine, applyFuzzyLayout };
1193
- //# sourceMappingURL=chunk-GPDX3O37.js.map
1194
- //# sourceMappingURL=chunk-GPDX3O37.js.map
1444
+ //# sourceMappingURL=chunk-YFJMQCGE.js.map
1445
+ //# sourceMappingURL=chunk-YFJMQCGE.js.map