@project-skymap/library 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,8 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  var THREE4 = require('three');
4
- var OrbitControls_js = require('three/examples/jsm/controls/OrbitControls.js');
5
- var DragControls_js = require('three/examples/jsm/controls/DragControls.js');
6
4
  var react = require('react');
7
5
  var jsxRuntime = require('react/jsx-runtime');
8
6
 
@@ -334,100 +332,80 @@ var init_layout = __esm({
334
332
  init_constellations();
335
333
  }
336
334
  });
337
- function matches(node, when) {
338
- for (const [k, v] of Object.entries(when)) {
339
- const val = node[k] !== void 0 ? node[k] : node.meta?.[k];
340
- if (val !== v) return false;
341
- }
342
- return true;
343
- }
344
- function getColorRules(cfg) {
345
- const anyCfg = cfg;
346
- const styleColors = anyCfg?.style?.colors;
347
- if (Array.isArray(styleColors)) return styleColors;
348
- const visualsColorBy = anyCfg?.visuals?.colorBy;
349
- if (Array.isArray(visualsColorBy)) {
350
- return visualsColorBy.map((r) => ({
351
- when: r.when ?? {},
352
- color: r.value,
353
- opacity: r.opacity
354
- }));
355
- }
356
- return [];
335
+
336
+ // src/engine/shaders.ts
337
+ var BLEND_CHUNK, MASK_CHUNK;
338
+ var init_shaders = __esm({
339
+ "src/engine/shaders.ts"() {
340
+ BLEND_CHUNK = `
341
+ uniform float uScale;
342
+ uniform float uAspect;
343
+ uniform float uBlend;
344
+
345
+ vec4 smartProject(vec4 viewPos) {
346
+ vec3 dir = normalize(viewPos.xyz);
347
+ float dist = length(viewPos.xyz);
348
+ float zLinear = max(0.01, -dir.z);
349
+ float kStereo = 2.0 / (1.0 - dir.z);
350
+ float kLinear = 1.0 / zLinear;
351
+ float k = mix(kLinear, kStereo, uBlend);
352
+ vec2 projected = vec2(k * dir.x, k * dir.y);
353
+ projected *= uScale;
354
+ projected.x /= uAspect;
355
+ float zMetric = -1.0 + (dist / 2000.0);
356
+ // Clip backward facing points in fisheye mode
357
+ if (uBlend > 0.5 && dir.z > 0.4) return vec4(10.0, 10.0, 10.0, 1.0);
358
+ // Clip very close points in linear mode
359
+ if (uBlend < 0.1 && dir.z > -0.1) return vec4(10.0, 10.0, 10.0, 1.0);
360
+ return vec4(projected, zMetric, 1.0);
357
361
  }
358
- function getSizeRules(cfg) {
359
- const anyCfg = cfg;
360
- const styleSizes = anyCfg?.style?.sizes;
361
- if (Array.isArray(styleSizes)) return styleSizes;
362
- const visualsSizeBy = anyCfg?.visuals?.sizeBy;
363
- if (Array.isArray(visualsSizeBy)) return visualsSizeBy;
364
- return [];
362
+ `;
363
+ MASK_CHUNK = `
364
+ uniform float uAspect;
365
+ uniform float uBlend;
366
+ varying vec2 vScreenPos;
367
+ float getMaskAlpha() {
368
+ if (uBlend < 0.1) return 1.0;
369
+ vec2 p = vScreenPos;
370
+ p.x *= uAspect;
371
+ float dist = length(p);
372
+ float t = smoothstep(0.75, 1.0, uBlend);
373
+ float currentRadius = mix(2.5, 1.0, t);
374
+ float edgeSoftness = mix(0.5, 0.02, t);
375
+ return 1.0 - smoothstep(currentRadius - edgeSoftness, currentRadius, dist);
365
376
  }
366
- function applyVisuals({
367
- model,
368
- cfg,
369
- meshById
370
- }) {
371
- const colorRules = getColorRules(cfg);
372
- const sizeRules = getSizeRules(cfg);
373
- const domains = {};
374
- for (const rule of sizeRules) {
375
- const key = JSON.stringify(rule.when) + "|" + rule.field;
376
- if (domains[key]) continue;
377
- if (rule.domain) {
378
- domains[key] = { min: rule.domain[0], max: rule.domain[1] };
379
- continue;
380
- }
381
- let min = Number.POSITIVE_INFINITY;
382
- let max = Number.NEGATIVE_INFINITY;
383
- for (const n of model.nodes) {
384
- if (!matches(n, rule.when)) continue;
385
- const v = n[rule.field] ?? n.meta?.[rule.field];
386
- if (typeof v !== "number" || !Number.isFinite(v)) continue;
387
- min = Math.min(min, v);
388
- max = Math.max(max, v);
389
- }
390
- if (!Number.isFinite(min) || !Number.isFinite(max)) {
391
- min = 0;
392
- max = 1;
393
- }
394
- domains[key] = { min, max };
395
- }
396
- for (const node of model.nodes) {
397
- const mesh = meshById.get(node.id);
398
- if (!mesh) continue;
399
- for (const rule of colorRules) {
400
- if (!matches(node, rule.when)) continue;
401
- mesh.traverse((obj) => {
402
- if (!obj.material) return;
403
- const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
404
- for (const m of mats) {
405
- if (m?.color) m.color = new THREE4__namespace.Color(rule.color);
406
- m.transparent = true;
407
- m.opacity = rule.opacity ?? 1;
408
- }
409
- });
410
- break;
411
- }
412
- for (const rule of sizeRules) {
413
- if (!matches(node, rule.when)) continue;
414
- const w = node[rule.field] ?? node.meta?.[rule.field];
415
- if (typeof w !== "number" || !Number.isFinite(w)) continue;
416
- const key = JSON.stringify(rule.when) + "|" + rule.field;
417
- const domain = domains[key];
418
- if (!domain) continue;
419
- const { min, max } = domain;
420
- const range = max - min;
421
- const t = range === 0 ? 0.5 : (w - min) / range;
422
- const clamped = Math.min(1, Math.max(0, t));
423
- const s = rule.scale[0] + clamped * (rule.scale[1] - rule.scale[0]);
424
- mesh.scale.setScalar(s);
425
- break;
426
- }
377
+ `;
427
378
  }
379
+ });
380
+ function createSmartMaterial(params) {
381
+ const uniforms = { ...globalUniforms, ...params.uniforms };
382
+ return new THREE4__namespace.ShaderMaterial({
383
+ uniforms,
384
+ vertexShader: `
385
+ ${BLEND_CHUNK}
386
+ varying vec2 vScreenPos;
387
+ ${params.vertexShaderBody}
388
+ `,
389
+ fragmentShader: `
390
+ ${MASK_CHUNK}
391
+ ${params.fragmentShader}
392
+ `,
393
+ transparent: params.transparent || false,
394
+ depthWrite: params.depthWrite !== void 0 ? params.depthWrite : true,
395
+ depthTest: params.depthTest !== void 0 ? params.depthTest : true,
396
+ side: params.side || THREE4__namespace.FrontSide,
397
+ blending: params.blending || THREE4__namespace.NormalBlending
398
+ });
428
399
  }
400
+ var globalUniforms;
429
401
  var init_materials = __esm({
430
402
  "src/engine/materials.ts"() {
403
+ init_shaders();
404
+ globalUniforms = {
405
+ uScale: { value: 1 },
406
+ uAspect: { value: 1 },
407
+ uBlend: { value: 0 }
408
+ };
431
409
  }
432
410
  });
433
411
 
@@ -442,380 +420,359 @@ function createEngine({
442
420
  onHover,
443
421
  onArrangementChange
444
422
  }) {
445
- const renderer = new THREE4__namespace.WebGLRenderer({ antialias: true });
423
+ const renderer = new THREE4__namespace.WebGLRenderer({ antialias: true, alpha: false });
446
424
  renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
447
- renderer.setClearColor(0, 1);
425
+ renderer.setSize(container.clientWidth, container.clientHeight);
448
426
  container.appendChild(renderer.domElement);
449
- renderer.domElement.style.width = "100%";
450
- renderer.domElement.style.height = "100%";
451
- renderer.domElement.style.touchAction = "none";
452
427
  const scene = new THREE4__namespace.Scene();
453
- const camera = new THREE4__namespace.PerspectiveCamera(90, 1, 0.1, 5e3);
454
- camera.position.set(0, 0, 0.01);
428
+ scene.background = new THREE4__namespace.Color(0);
429
+ const camera = new THREE4__namespace.PerspectiveCamera(60, 1, 0.1, 3e3);
430
+ camera.position.set(0, 0, 0);
455
431
  camera.up.set(0, 1, 0);
456
- const controls = new OrbitControls_js.OrbitControls(camera, renderer.domElement);
457
- controls.target.set(0, 0, 0);
458
- controls.enableRotate = true;
459
- controls.enablePan = false;
460
- controls.enableZoom = false;
461
- controls.enableDamping = true;
462
- controls.dampingFactor = 0.07;
463
- controls.screenSpacePanning = false;
464
- const EPS = THREE4__namespace.MathUtils.degToRad(0.05);
465
- controls.minAzimuthAngle = -Infinity;
466
- controls.maxAzimuthAngle = Infinity;
467
- controls.minPolarAngle = Math.PI / 2 + EPS;
468
- controls.maxPolarAngle = Math.PI - EPS;
469
- controls.update();
470
- const env = {
471
- skyRadius: 2800,
472
- groundRadius: 995,
473
- timeScale: 60,
474
- // 1 = real-time; 60 = 60x faster drift
475
- defaultFov: 90,
476
- minFov: 6,
477
- maxFov: 110,
478
- // 0.02 works well for mouse wheels; trackpads are moderated via ctrlKey
479
- fovWheelSensitivity: 0.02,
480
- zoomLerp: 0.12,
481
- focusZoomFov: 18,
482
- focusDurationMs: 650};
483
- const SIDEREAL_RATE = THREE4__namespace.MathUtils.degToRad(15) / 3600;
484
- let lastFrameMs = performance.now();
485
- let targetFov = env.defaultFov;
486
- let zoomOutLimitFov = env.defaultFov;
432
+ let running = false;
433
+ let raf = 0;
434
+ const state = {
435
+ isDragging: false,
436
+ lastMouseX: 0,
437
+ lastMouseY: 0,
438
+ velocityX: 0,
439
+ velocityY: 0,
440
+ lat: 0.5,
441
+ lon: 0,
442
+ targetLat: 0.5,
443
+ targetLon: 0,
444
+ fov: ENGINE_CONFIG.defaultFov,
445
+ dragMode: "none",
446
+ draggedNodeId: null,
447
+ draggedStarIndex: -1,
448
+ draggedDist: 2e3,
449
+ draggedGroup: null
450
+ };
451
+ const mouseNDC = new THREE4__namespace.Vector2();
452
+ let isMouseInWindow = false;
453
+ let handlers = { onSelect, onHover, onArrangementChange };
454
+ let currentConfig;
455
+ function mix(a, b, t) {
456
+ return a * (1 - t) + b * t;
457
+ }
458
+ function getBlendFactor(fov) {
459
+ if (fov <= ENGINE_CONFIG.blendStart) return 0;
460
+ if (fov >= ENGINE_CONFIG.blendEnd) return 1;
461
+ let t = (fov - ENGINE_CONFIG.blendStart) / (ENGINE_CONFIG.blendEnd - ENGINE_CONFIG.blendStart);
462
+ return t * t * (3 - 2 * t);
463
+ }
464
+ function updateUniforms() {
465
+ const blend = getBlendFactor(state.fov);
466
+ globalUniforms.uBlend.value = blend;
467
+ const fovRad = state.fov * Math.PI / 180;
468
+ const scaleLinear = 1 / Math.tan(fovRad / 2);
469
+ const scaleStereo = 1 / (2 * Math.tan(fovRad / 4));
470
+ globalUniforms.uScale.value = mix(scaleLinear, scaleStereo, blend);
471
+ globalUniforms.uAspect.value = camera.aspect;
472
+ camera.fov = Math.min(state.fov, ENGINE_CONFIG.defaultFov);
473
+ camera.updateProjectionMatrix();
474
+ }
475
+ function getMouseViewVector(fovDeg, aspectRatio) {
476
+ const blend = getBlendFactor(fovDeg);
477
+ const fovRad = fovDeg * Math.PI / 180;
478
+ const uvX = mouseNDC.x * aspectRatio;
479
+ const uvY = mouseNDC.y;
480
+ const r_uv = Math.sqrt(uvX * uvX + uvY * uvY);
481
+ const halfHeightLinear = Math.tan(fovRad / 2);
482
+ const theta_lin = Math.atan(r_uv * halfHeightLinear);
483
+ const halfHeightStereo = 2 * Math.tan(fovRad / 4);
484
+ const theta_str = 2 * Math.atan(r_uv * halfHeightStereo / 2);
485
+ const theta = mix(theta_lin, theta_str, blend);
486
+ const phi = Math.atan2(uvY, uvX);
487
+ const sinTheta = Math.sin(theta);
488
+ const cosTheta = Math.cos(theta);
489
+ return new THREE4__namespace.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
490
+ }
491
+ function getMouseWorldVector(pixelX, pixelY, width, height) {
492
+ const aspect = width / height;
493
+ const ndcX = pixelX / width * 2 - 1;
494
+ const ndcY = -(pixelY / height) * 2 + 1;
495
+ const blend = getBlendFactor(state.fov);
496
+ const fovRad = state.fov * Math.PI / 180;
497
+ const uvX = ndcX * aspect;
498
+ const uvY = ndcY;
499
+ const r_uv = Math.sqrt(uvX * uvX + uvY * uvY);
500
+ const halfHeightLinear = Math.tan(fovRad / 2);
501
+ const theta_lin = Math.atan(r_uv * halfHeightLinear);
502
+ const halfHeightStereo = 2 * Math.tan(fovRad / 4);
503
+ const theta_str = 2 * Math.atan(r_uv * halfHeightStereo / 2);
504
+ const theta = mix(theta_lin, theta_str, blend);
505
+ const phi = Math.atan2(uvY, uvX);
506
+ const sinTheta = Math.sin(theta);
507
+ const cosTheta = Math.cos(theta);
508
+ const vView = new THREE4__namespace.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
509
+ return vView.applyQuaternion(camera.quaternion);
510
+ }
511
+ function smartProjectJS(worldPos) {
512
+ const viewPos = worldPos.clone().applyMatrix4(camera.matrixWorldInverse);
513
+ const dir = viewPos.clone().normalize();
514
+ const zLinear = Math.max(0.01, -dir.z);
515
+ const kStereo = 2 / (1 - dir.z);
516
+ const kLinear = 1 / zLinear;
517
+ const blend = globalUniforms.uBlend.value;
518
+ const k = mix(kLinear, kStereo, blend);
519
+ return { x: k * dir.x, y: k * dir.y, z: dir.z };
520
+ }
487
521
  const groundGroup = new THREE4__namespace.Group();
488
522
  scene.add(groundGroup);
489
- function buildGroundHemisphere(radius) {
523
+ function createGround() {
490
524
  groundGroup.clear();
491
- const hemi = new THREE4__namespace.SphereGeometry(radius, 64, 32, 0, Math.PI * 2, Math.PI / 2, Math.PI);
492
- hemi.scale(-1, 1, 1);
493
- const count = hemi.attributes.position.count;
494
- const colors = new Float32Array(count * 3);
495
- const pos = hemi.attributes.position;
496
- const c = new THREE4__namespace.Color();
497
- for (let i = 0; i < count; i++) {
498
- const y = pos.getY(i);
499
- const t = THREE4__namespace.MathUtils.clamp(1 - Math.abs(y) / radius, 0, 1);
500
- c.setRGB(
501
- THREE4__namespace.MathUtils.lerp(6 / 255, 21 / 255, t),
502
- THREE4__namespace.MathUtils.lerp(16 / 255, 35 / 255, t),
503
- THREE4__namespace.MathUtils.lerp(23 / 255, 53 / 255, t)
504
- );
505
- colors.set([c.r, c.g, c.b], i * 3);
506
- }
507
- hemi.setAttribute("color", new THREE4__namespace.BufferAttribute(colors, 3));
508
- const groundMat = new THREE4__namespace.MeshBasicMaterial({
509
- vertexColors: true,
510
- side: THREE4__namespace.FrontSide,
525
+ const radius = 995;
526
+ const geometry = new THREE4__namespace.SphereGeometry(radius, 128, 64, 0, Math.PI * 2, Math.PI / 2, Math.PI / 2);
527
+ const material = createSmartMaterial({
528
+ uniforms: { color: { value: new THREE4__namespace.Color(526862) } },
529
+ vertexShaderBody: `varying vec3 vPos; void main() { vPos = position; vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_Position = smartProject(mvPosition); vScreenPos = gl_Position.xy / gl_Position.w; }`,
530
+ fragmentShader: `uniform vec3 color; varying vec3 vPos; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; float noise = sin(vPos.x * 0.2) * sin(vPos.z * 0.2) * 0.05; vec3 col = color + noise; vec3 n = normalize(vPos); float horizon = smoothstep(-0.02, 0.0, n.y); col += vec3(0.1, 0.15, 0.2) * horizon; gl_FragColor = vec4(col, 1.0); }`,
531
+ side: THREE4__namespace.BackSide,
532
+ transparent: false,
511
533
  depthWrite: true,
512
- // important: occlude stars under horizon
513
534
  depthTest: true
514
535
  });
515
- const hemiMesh = new THREE4__namespace.Mesh(hemi, groundMat);
516
- groundGroup.add(hemiMesh);
517
- {
518
- const inner = radius * 0.985;
519
- const outer = radius * 1.005;
520
- const ringGeo = new THREE4__namespace.RingGeometry(inner, outer, 128);
521
- ringGeo.rotateX(-Math.PI / 2);
522
- const ringMat = new THREE4__namespace.ShaderMaterial({
523
- transparent: true,
524
- depthWrite: false,
525
- uniforms: {
526
- uInner: { value: inner },
527
- uOuter: { value: outer },
528
- uColor: { value: new THREE4__namespace.Color(8037119) }
529
- },
530
- vertexShader: `
531
- varying vec2 vXY;
532
- void main() {
533
- vXY = position.xz;
534
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
535
- }
536
- `,
537
- fragmentShader: `
538
- uniform float uInner;
539
- uniform float uOuter;
540
- uniform vec3 uColor;
541
- varying vec2 vXY;
542
-
543
- void main() {
544
- float r = length(vXY);
545
- float t = clamp((r - uInner) / (uOuter - uInner), 0.0, 1.0);
546
- // peak glow near inner edge, fade outwards
547
- float a = pow(1.0 - t, 2.2) * 0.35;
548
- gl_FragColor = vec4(uColor, a);
549
- }
550
- `
551
- });
552
- const ring = new THREE4__namespace.Mesh(ringGeo, ringMat);
553
- ring.position.y = 0;
554
- groundGroup.add(ring);
536
+ const ground = new THREE4__namespace.Mesh(geometry, material);
537
+ groundGroup.add(ground);
538
+ const boxGeo = new THREE4__namespace.BoxGeometry(8, 30, 8);
539
+ for (let i = 0; i < 12; i++) {
540
+ const angle = i / 12 * Math.PI * 2;
541
+ const b = new THREE4__namespace.Mesh(boxGeo, material);
542
+ const r = radius * 0.98;
543
+ b.position.set(Math.cos(angle) * r, -15, Math.sin(angle) * r);
544
+ b.lookAt(0, 0, 0);
545
+ groundGroup.add(b);
555
546
  }
556
547
  }
557
- buildGroundHemisphere(env.groundRadius);
548
+ function createAtmosphere() {
549
+ const geometry = new THREE4__namespace.SphereGeometry(990, 128, 64);
550
+ const material = createSmartMaterial({
551
+ uniforms: { top: { value: new THREE4__namespace.Color(0) }, bot: { value: new THREE4__namespace.Color(1712172) } },
552
+ vertexShaderBody: `
553
+
554
+ varying vec3 vP;
555
+
556
+ void main() {
557
+
558
+ vP = position;
559
+
560
+ vec4 mv = modelViewMatrix * vec4(position, 1.0);
561
+
562
+ gl_Position = smartProject(mv);
563
+
564
+ vScreenPos = gl_Position.xy / gl_Position.w;
565
+
566
+ }
567
+
568
+ `,
569
+ fragmentShader: `
570
+
571
+ uniform vec3 top;
572
+
573
+ uniform vec3 bot;
574
+
575
+ varying vec3 vP;
576
+
577
+ void main() {
578
+
579
+ float alphaMask = getMaskAlpha();
580
+
581
+ if (alphaMask < 0.01) discard;
582
+
583
+ vec3 n = normalize(vP);
584
+
585
+ float h = max(0.0, n.y);
586
+
587
+ gl_FragColor = vec4(mix(bot, top, pow(h, 0.6)), 1.0);
588
+
589
+ }
590
+
591
+ `,
592
+ side: THREE4__namespace.BackSide,
593
+ depthWrite: false,
594
+ depthTest: true
595
+ });
596
+ const atm = new THREE4__namespace.Mesh(geometry, material);
597
+ groundGroup.add(atm);
598
+ }
558
599
  const backdropGroup = new THREE4__namespace.Group();
559
600
  scene.add(backdropGroup);
560
- function buildBackdropStars(radius, count) {
601
+ function createBackdropStars() {
561
602
  backdropGroup.clear();
562
- const starGeo = new THREE4__namespace.BufferGeometry();
563
- const starPos = new Float32Array(count * 3);
564
- for (let i = 0; i < count; i++) {
565
- const r = radius + Math.random() * (radius * 0.5);
566
- const theta = Math.random() * Math.PI * 2;
567
- const phi = Math.acos(2 * Math.random() - 1);
568
- starPos[i * 3] = r * Math.sin(phi) * Math.cos(theta);
569
- starPos[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
570
- starPos[i * 3 + 2] = r * Math.cos(phi);
603
+ const geometry = new THREE4__namespace.BufferGeometry();
604
+ const positions = [];
605
+ const sizes = [];
606
+ const colors = [];
607
+ const colorPalette = [
608
+ new THREE4__namespace.Color(10203391),
609
+ new THREE4__namespace.Color(11190271),
610
+ new THREE4__namespace.Color(13293567),
611
+ new THREE4__namespace.Color(16316415),
612
+ new THREE4__namespace.Color(16774378),
613
+ new THREE4__namespace.Color(16765601),
614
+ new THREE4__namespace.Color(16764015)
615
+ ];
616
+ const r = 2500;
617
+ new THREE4__namespace.Vector3(0, 1, 0.5).normalize();
618
+ for (let i = 0; i < 4e3; i++) {
619
+ const isMilkyWay = Math.random() < 0.4;
620
+ let x, y, z;
621
+ if (isMilkyWay) {
622
+ const theta = Math.random() * Math.PI * 2;
623
+ const scatter = (Math.random() - 0.5) * 0.4;
624
+ const v = new THREE4__namespace.Vector3(Math.cos(theta), scatter, Math.sin(theta));
625
+ v.normalize();
626
+ v.applyAxisAngle(new THREE4__namespace.Vector3(1, 0, 0), THREE4__namespace.MathUtils.degToRad(60));
627
+ x = v.x * r;
628
+ y = v.y * r;
629
+ z = v.z * r;
630
+ } else {
631
+ const u = Math.random();
632
+ const v = Math.random();
633
+ const theta = 2 * Math.PI * u;
634
+ const phi = Math.acos(2 * v - 1);
635
+ x = r * Math.sin(phi) * Math.cos(theta);
636
+ y = r * Math.sin(phi) * Math.sin(theta);
637
+ z = r * Math.cos(phi);
638
+ }
639
+ positions.push(x, y, z);
640
+ const size = 0.5 + -Math.log(Math.random()) * 0.8 * 1.5;
641
+ sizes.push(size);
642
+ const cIndex = Math.floor(Math.random() * colorPalette.length);
643
+ const c = colorPalette[cIndex];
644
+ colors.push(c.r, c.g, c.b);
571
645
  }
572
- starGeo.setAttribute("position", new THREE4__namespace.BufferAttribute(starPos, 3));
573
- const starMat = new THREE4__namespace.PointsMaterial({
574
- color: 10135218,
575
- size: 0.5,
646
+ geometry.setAttribute("position", new THREE4__namespace.Float32BufferAttribute(positions, 3));
647
+ geometry.setAttribute("size", new THREE4__namespace.Float32BufferAttribute(sizes, 1));
648
+ geometry.setAttribute("color", new THREE4__namespace.Float32BufferAttribute(colors, 3));
649
+ const material = createSmartMaterial({
650
+ uniforms: { pixelRatio: { value: renderer.getPixelRatio() } },
651
+ vertexShaderBody: `
652
+ attribute float size;
653
+ attribute vec3 color;
654
+ varying vec3 vColor;
655
+ uniform float pixelRatio;
656
+ void main() {
657
+ vColor = color;
658
+ vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
659
+ gl_Position = smartProject(mvPosition);
660
+ vScreenPos = gl_Position.xy / gl_Position.w;
661
+ gl_PointSize = size * pixelRatio * (600.0 / -mvPosition.z);
662
+ }
663
+ `,
664
+ fragmentShader: `
665
+ varying vec3 vColor;
666
+ void main() {
667
+ vec2 coord = gl_PointCoord - vec2(0.5);
668
+ float dist = length(coord) * 2.0;
669
+ if (dist > 1.0) discard;
670
+ float alphaMask = getMaskAlpha();
671
+ if (alphaMask < 0.01) discard;
672
+ // Use same Gaussian glow for backdrop
673
+ float alpha = exp(-3.0 * dist * dist);
674
+ gl_FragColor = vec4(vColor, alpha * alphaMask);
675
+ }
676
+ `,
576
677
  transparent: true,
577
- opacity: 0.55,
578
- depthWrite: false
678
+ depthWrite: false,
679
+ depthTest: true
579
680
  });
580
- const stars = new THREE4__namespace.Points(starGeo, starMat);
581
- backdropGroup.add(stars);
681
+ const points = new THREE4__namespace.Points(geometry, material);
682
+ points.frustumCulled = false;
683
+ backdropGroup.add(points);
582
684
  }
583
- buildBackdropStars(env.skyRadius, 6500);
685
+ createGround();
686
+ createAtmosphere();
687
+ createBackdropStars();
584
688
  const raycaster = new THREE4__namespace.Raycaster();
585
- const pointer = new THREE4__namespace.Vector2();
689
+ raycaster.params.Points.threshold = 5;
690
+ new THREE4__namespace.Vector2();
586
691
  const root = new THREE4__namespace.Group();
587
692
  scene.add(root);
588
- let raf = 0;
589
- let running = false;
590
- let handlers = { onSelect, onHover, onArrangementChange };
591
- let hoveredId = null;
592
- let isDragging = false;
593
- let dragControls = null;
594
- let currentConfig;
595
693
  const nodeById = /* @__PURE__ */ new Map();
596
- const meshById = /* @__PURE__ */ new Map();
597
- const lineByBookId = /* @__PURE__ */ new Map();
598
- const dynamicObjects = [];
599
- function getFullArrangement() {
600
- const arr = {};
601
- for (const [id, mesh] of meshById.entries()) {
602
- arr[id] = {
603
- position: [mesh.position.x, mesh.position.y, mesh.position.z]
604
- };
605
- }
606
- return arr;
607
- }
608
- function updateBookLine(bookId) {
609
- const line = lineByBookId.get(bookId);
610
- if (!line) return;
611
- const chapters = [];
612
- for (const n of nodeById.values()) {
613
- if (n.parent === bookId && n.level === 3) {
614
- chapters.push(n);
615
- }
616
- }
617
- chapters.sort((a, b) => {
618
- const cA = a.meta?.chapter || 0;
619
- const cB = b.meta?.chapter || 0;
620
- return cA - cB;
621
- });
622
- const points = [];
623
- for (const c of chapters) {
624
- const m = meshById.get(c.id);
625
- if (m) {
626
- points.push(m.position.clone());
627
- }
628
- }
629
- if (points.length > 1) {
630
- line.geometry.setFromPoints(points);
631
- }
632
- }
633
- function updateDragControls(editable) {
634
- if (!editable) {
635
- if (dragControls) {
636
- dragControls.dispose();
637
- dragControls = null;
638
- }
639
- return;
640
- }
641
- const draggables = [];
642
- for (const [id, mesh] of meshById.entries()) {
643
- const node = nodeById.get(id);
644
- if (node?.level === 3) {
645
- draggables.push(mesh);
646
- } else if (node?.level === 2 && mesh instanceof THREE4__namespace.Sprite) {
647
- draggables.push(mesh);
694
+ const starIndexToId = [];
695
+ const dynamicLabels = [];
696
+ let constellationLines = null;
697
+ let starPoints = null;
698
+ function clearRoot() {
699
+ for (const child of [...root.children]) {
700
+ root.remove(child);
701
+ if (child.geometry) child.geometry.dispose();
702
+ if (child.material) {
703
+ const m = child.material;
704
+ if (Array.isArray(m)) m.forEach((mm) => mm.dispose());
705
+ else m.dispose();
648
706
  }
649
707
  }
650
- if (dragControls) {
651
- dragControls.dispose();
652
- }
653
- dragControls = new DragControls_js.DragControls(draggables, camera, renderer.domElement);
654
- const lastPos = new THREE4__namespace.Vector3();
655
- dragControls.addEventListener("dragstart", (event) => {
656
- controls.enabled = false;
657
- isDragging = true;
658
- lastPos.copy(event.object.position);
659
- });
660
- dragControls.addEventListener("drag", (event) => {
661
- const obj = event.object;
662
- const id = obj.userData.id;
663
- const node = nodeById.get(id);
664
- if (node && node.level === 2) {
665
- const currentPos = obj.position;
666
- const delta = new THREE4__namespace.Vector3().subVectors(currentPos, lastPos);
667
- const bookId = id;
668
- const childStars = [];
669
- for (const [nId, n] of nodeById.entries()) {
670
- if (n.parent === bookId && n.level === 3) {
671
- const starMesh = meshById.get(nId);
672
- if (starMesh) childStars.push(starMesh);
673
- }
674
- }
675
- for (const star of childStars) {
676
- star.position.add(delta);
677
- }
678
- updateBookLine(bookId);
679
- lastPos.copy(currentPos);
680
- } else if (node && node.level === 3) {
681
- if (node.parent) {
682
- updateBookLine(node.parent);
683
- }
684
- }
685
- });
686
- dragControls.addEventListener("dragend", (event) => {
687
- controls.enabled = true;
688
- setTimeout(() => {
689
- isDragging = false;
690
- }, 0);
691
- const obj = event.object;
692
- const id = obj.userData.id;
693
- if (id && currentConfig) {
694
- handlers.onArrangementChange?.(getFullArrangement());
695
- }
696
- });
697
- }
698
- function resize() {
699
- const w = container.clientWidth || 1;
700
- const h = container.clientHeight || 1;
701
- renderer.setSize(w, h, false);
702
- camera.aspect = w / h;
703
- camera.updateProjectionMatrix();
704
- }
705
- function disposeObject(obj) {
706
- obj.traverse((o) => {
707
- if (o.geometry) o.geometry.dispose?.();
708
- if (o.material) {
709
- const mats = Array.isArray(o.material) ? o.material : [o.material];
710
- mats.forEach((m) => {
711
- if (m.map) m.map.dispose?.();
712
- m.dispose?.();
713
- });
714
- }
715
- });
708
+ nodeById.clear();
709
+ starIndexToId.length = 0;
710
+ dynamicLabels.length = 0;
711
+ constellationLines = null;
712
+ starPoints = null;
716
713
  }
717
- function createTextSprite(text, color = "#ffffff") {
714
+ function createTextTexture(text, color = "#ffffff") {
718
715
  const canvas = document.createElement("canvas");
719
716
  const ctx = canvas.getContext("2d");
720
717
  if (!ctx) return null;
721
718
  const fontSize = 96;
722
- const font = `bold ${fontSize}px sans-serif`;
723
- ctx.font = font;
719
+ ctx.font = `bold ${fontSize}px sans-serif`;
724
720
  const metrics = ctx.measureText(text);
725
721
  const w = Math.ceil(metrics.width);
726
722
  const h = Math.ceil(fontSize * 1.2);
727
723
  canvas.width = w;
728
724
  canvas.height = h;
729
- ctx.font = font;
725
+ ctx.font = `bold ${fontSize}px sans-serif`;
730
726
  ctx.fillStyle = color;
731
727
  ctx.textAlign = "center";
732
728
  ctx.textBaseline = "middle";
733
729
  ctx.fillText(text, w / 2, h / 2);
734
730
  const tex = new THREE4__namespace.CanvasTexture(canvas);
735
731
  tex.minFilter = THREE4__namespace.LinearFilter;
736
- const mat = new THREE4__namespace.SpriteMaterial({
737
- map: tex,
738
- transparent: true,
739
- depthWrite: false,
740
- depthTest: true
741
- });
742
- const sprite = new THREE4__namespace.Sprite(mat);
743
- const targetHeight = 4;
744
- const aspect = w / h;
745
- sprite.scale.set(targetHeight * aspect, targetHeight, 1);
746
- return sprite;
732
+ return { tex, aspect: w / h };
747
733
  }
748
- function createStarTexture() {
749
- const canvas = document.createElement("canvas");
750
- canvas.width = 64;
751
- canvas.height = 64;
752
- const ctx = canvas.getContext("2d");
753
- const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
754
- gradient.addColorStop(0, "rgba(255, 255, 255, 1)");
755
- gradient.addColorStop(0.3, "rgba(255, 255, 255, 0.9)");
756
- gradient.addColorStop(0.6, "rgba(255, 255, 255, 0.4)");
757
- gradient.addColorStop(1, "rgba(255, 255, 255, 0)");
758
- ctx.fillStyle = gradient;
759
- ctx.fillRect(0, 0, 64, 64);
760
- const tex = new THREE4__namespace.CanvasTexture(canvas);
761
- return tex;
762
- }
763
- const starTexture = createStarTexture();
764
- function clearRoot() {
765
- for (const child of [...root.children]) {
766
- root.remove(child);
767
- disposeObject(child);
734
+ function getPosition(n) {
735
+ if (currentConfig?.arrangement) {
736
+ const arr = currentConfig.arrangement[n.id];
737
+ if (arr) return new THREE4__namespace.Vector3(arr.position[0], arr.position[1], arr.position[2]);
768
738
  }
769
- nodeById.clear();
770
- meshById.clear();
771
- lineByBookId.clear();
772
- dynamicObjects.length = 0;
739
+ return new THREE4__namespace.Vector3(n.meta?.x ?? 0, n.meta?.y ?? 0, n.meta?.z ?? 0);
740
+ }
741
+ function getBoundaryPoint(angle, t, radius) {
742
+ const y = 0.05 + t * (1 - 0.05);
743
+ const rY = Math.sqrt(1 - y * y);
744
+ const x = Math.cos(angle) * rY;
745
+ const z = Math.sin(angle) * rY;
746
+ return new THREE4__namespace.Vector3(x, y, z).multiplyScalar(radius);
773
747
  }
774
748
  function buildFromModel(model, cfg) {
775
749
  clearRoot();
776
- if (cfg.background && cfg.background !== "transparent") {
777
- scene.background = new THREE4__namespace.Color(cfg.background);
778
- } else {
779
- scene.background = null;
780
- }
781
- camera.fov = cfg.camera?.fov ?? env.defaultFov;
782
- camera.updateProjectionMatrix();
783
- targetFov = camera.fov;
784
- zoomOutLimitFov = camera.fov;
785
- const layoutCfg = {
786
- ...cfg.layout,
787
- radius: cfg.layout?.radius ?? 2e3
788
- };
750
+ scene.background = cfg.background && cfg.background !== "transparent" ? new THREE4__namespace.Color(cfg.background) : new THREE4__namespace.Color(0);
751
+ const layoutCfg = { ...cfg.layout, radius: cfg.layout?.radius ?? 2e3 };
789
752
  const laidOut = computeLayoutPositions(model, layoutCfg);
790
- const boundaries = laidOut.meta?.divisionBoundaries ?? [];
791
- if (boundaries.length > 0) {
792
- const boundaryMat = new THREE4__namespace.LineBasicMaterial({
793
- color: 5601177,
794
- transparent: true,
795
- opacity: 0.15,
796
- depthWrite: false,
797
- blending: THREE4__namespace.AdditiveBlending
798
- });
799
- boundaries.forEach((angle) => {
800
- const points = [];
801
- const steps = 32;
802
- for (let i = 0; i <= steps; i++) {
803
- const t = i / steps;
804
- const y = 0.05 + t * (1 - 0.05);
805
- const rY = Math.sqrt(1 - y * y);
806
- const x = Math.cos(angle) * rY;
807
- const z = Math.sin(angle) * rY;
808
- const pos = new THREE4__namespace.Vector3(x, y, z).multiplyScalar(layoutCfg.radius);
809
- points.push(pos);
810
- }
811
- const geo = new THREE4__namespace.BufferGeometry().setFromPoints(points);
812
- const line = new THREE4__namespace.Line(geo, boundaryMat);
813
- root.add(line);
814
- });
815
- }
753
+ const starPositions = [];
754
+ const starSizes = [];
755
+ const starColors = [];
756
+ const SPECTRAL_COLORS = [
757
+ new THREE4__namespace.Color(10203391),
758
+ // O - Blue
759
+ new THREE4__namespace.Color(11190271),
760
+ // B - Blue-white
761
+ new THREE4__namespace.Color(13293567),
762
+ // A - White-blue
763
+ new THREE4__namespace.Color(16316415),
764
+ // F - White
765
+ new THREE4__namespace.Color(16774378),
766
+ // G - Yellow-white
767
+ new THREE4__namespace.Color(16765601),
768
+ // K - Yellow-orange
769
+ new THREE4__namespace.Color(16764015)
770
+ // M - Orange-red
771
+ ];
816
772
  let minWeight = Infinity;
817
773
  let maxWeight = -Infinity;
818
774
  for (const n of laidOut.nodes) {
775
+ nodeById.set(n.id, n);
819
776
  if (n.level === 3 && typeof n.weight === "number") {
820
777
  if (n.weight < minWeight) minWeight = n.weight;
821
778
  if (n.weight > maxWeight) maxWeight = n.weight;
@@ -828,75 +785,113 @@ function createEngine({
828
785
  maxWeight = minWeight + 1;
829
786
  }
830
787
  for (const n of laidOut.nodes) {
831
- nodeById.set(n.id, n);
832
- let x = n.meta?.x ?? 0;
833
- let y = n.meta?.y ?? 0;
834
- let z = n.meta?.z ?? 0;
835
- if (cfg.arrangement) {
836
- const arr = cfg.arrangement[n.id];
837
- if (arr) {
838
- const pos = arr.position;
839
- x = pos[0];
840
- y = pos[1];
841
- z = pos[2];
842
- }
843
- }
844
788
  if (n.level === 3) {
845
- const mat = new THREE4__namespace.SpriteMaterial({
846
- map: starTexture,
847
- color: 16777215,
848
- transparent: true,
849
- blending: THREE4__namespace.AdditiveBlending,
850
- depthWrite: false
851
- });
852
- const sprite = new THREE4__namespace.Sprite(mat);
853
- sprite.position.set(x, y, z);
854
- sprite.userData = { id: n.id, level: n.level };
855
- let baseScale = 3.5;
789
+ const p = getPosition(n);
790
+ starPositions.push(p.x, p.y, p.z);
791
+ starIndexToId.push(n.id);
792
+ let baseSize = 3.5;
856
793
  if (typeof n.weight === "number") {
857
794
  const t = (n.weight - minWeight) / (maxWeight - minWeight);
858
- baseScale = 3 + t * 4;
859
- }
860
- sprite.scale.setScalar(baseScale);
861
- dynamicObjects.push({ obj: sprite, initialScale: sprite.scale.clone(), type: "star" });
862
- if (n.label) {
863
- const labelSprite = createTextSprite(n.label);
864
- if (labelSprite) {
865
- labelSprite.position.set(0, 3, 0);
866
- labelSprite.scale.multiplyScalar(1.33);
867
- labelSprite.visible = false;
868
- sprite.add(labelSprite);
869
- }
795
+ baseSize = 3 + t * 4;
870
796
  }
871
- root.add(sprite);
872
- meshById.set(n.id, sprite);
873
- } else if (n.level === 1 || n.level === 2) {
874
- if (n.label) {
875
- const isBook = n.level === 2;
876
- const color = isBook ? "#ffffff" : "#38bdf8";
877
- const baseScale = isBook ? 6 : 4.66;
878
- const labelSprite = createTextSprite(n.label, color);
879
- if (labelSprite) {
880
- labelSprite.position.set(x, y, z);
881
- labelSprite.scale.multiplyScalar(baseScale);
882
- root.add(labelSprite);
883
- if (isBook) {
884
- labelSprite.userData = { id: n.id, level: n.level, interactive: true };
885
- meshById.set(n.id, labelSprite);
886
- dynamicObjects.push({ obj: labelSprite, initialScale: labelSprite.scale.clone(), type: "label" });
887
- }
888
- }
797
+ starSizes.push(baseSize);
798
+ const colorIdx = Math.floor(Math.pow(Math.random(), 1.5) * SPECTRAL_COLORS.length);
799
+ const c = SPECTRAL_COLORS[Math.min(colorIdx, SPECTRAL_COLORS.length - 1)];
800
+ starColors.push(c.r, c.g, c.b);
801
+ } else if (n.level === 2) {
802
+ const color = "#ffffff";
803
+ const texRes = createTextTexture(n.label, color);
804
+ if (texRes) {
805
+ const baseScale = 0.05;
806
+ const size = new THREE4__namespace.Vector2(baseScale * texRes.aspect, baseScale);
807
+ const mat = createSmartMaterial({
808
+ uniforms: {
809
+ uMap: { value: texRes.tex },
810
+ uSize: { value: size },
811
+ uAlpha: { value: 0 }
812
+ },
813
+ vertexShaderBody: `
814
+ uniform vec2 uSize;
815
+ varying vec2 vUv;
816
+ void main() {
817
+ vUv = uv;
818
+ vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
819
+ vec4 projected = smartProject(mvPos);
820
+ vec2 offset = position.xy * uSize;
821
+ projected.xy += offset / vec2(uAspect, 1.0);
822
+ gl_Position = projected;
823
+ }
824
+ `,
825
+ fragmentShader: `
826
+ uniform sampler2D uMap;
827
+ uniform float uAlpha;
828
+ varying vec2 vUv;
829
+ void main() {
830
+ float mask = getMaskAlpha();
831
+ if (mask < 0.01) discard;
832
+ vec4 tex = texture2D(uMap, vUv);
833
+ gl_FragColor = vec4(tex.rgb, tex.a * uAlpha * mask);
834
+ }
835
+ `,
836
+ transparent: true,
837
+ depthWrite: false,
838
+ depthTest: true
839
+ });
840
+ const mesh = new THREE4__namespace.Mesh(new THREE4__namespace.PlaneGeometry(1, 1), mat);
841
+ const p = getPosition(n);
842
+ mesh.position.set(p.x, p.y, p.z);
843
+ mesh.scale.set(size.x, size.y, 1);
844
+ mesh.frustumCulled = false;
845
+ mesh.userData = { id: n.id };
846
+ root.add(mesh);
847
+ dynamicLabels.push({ obj: mesh, node: n, initialScale: size.clone() });
889
848
  }
890
849
  }
891
850
  }
892
- applyVisuals({ model: laidOut, cfg, meshById });
893
- const lineMat = new THREE4__namespace.LineBasicMaterial({
894
- color: 4478310,
851
+ const starGeo = new THREE4__namespace.BufferGeometry();
852
+ starGeo.setAttribute("position", new THREE4__namespace.Float32BufferAttribute(starPositions, 3));
853
+ starGeo.setAttribute("size", new THREE4__namespace.Float32BufferAttribute(starSizes, 1));
854
+ starGeo.setAttribute("color", new THREE4__namespace.Float32BufferAttribute(starColors, 3));
855
+ const starMat = createSmartMaterial({
856
+ uniforms: { pixelRatio: { value: renderer.getPixelRatio() } },
857
+ vertexShaderBody: `
858
+ attribute float size;
859
+ attribute vec3 color;
860
+ varying vec3 vColor;
861
+ uniform float pixelRatio;
862
+ void main() {
863
+ vColor = color;
864
+ vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
865
+ gl_Position = smartProject(mvPosition);
866
+ vScreenPos = gl_Position.xy / gl_Position.w;
867
+ gl_PointSize = size * pixelRatio * (2000.0 / -mvPosition.z);
868
+ }
869
+ `,
870
+ fragmentShader: `
871
+ varying vec3 vColor;
872
+ void main() {
873
+ vec2 coord = gl_PointCoord - vec2(0.5);
874
+ // Use larger drawing area for glow
875
+ float dist = length(coord) * 2.0;
876
+ if (dist > 1.0) discard;
877
+
878
+ float alphaMask = getMaskAlpha();
879
+ if (alphaMask < 0.01) discard;
880
+
881
+ // Gaussian Glow: Sharp core, soft halo
882
+ float alpha = exp(-3.0 * dist * dist);
883
+
884
+ gl_FragColor = vec4(vColor, alpha * alphaMask);
885
+ }
886
+ `,
895
887
  transparent: true,
896
- opacity: 0.3,
897
888
  depthWrite: false,
898
- blending: THREE4__namespace.AdditiveBlending
889
+ depthTest: true
899
890
  });
891
+ starPoints = new THREE4__namespace.Points(starGeo, starMat);
892
+ starPoints.frustumCulled = false;
893
+ root.add(starPoints);
894
+ const linePoints = [];
900
895
  const bookMap = /* @__PURE__ */ new Map();
901
896
  for (const n of laidOut.nodes) {
902
897
  if (n.level === 3 && n.parent) {
@@ -905,94 +900,63 @@ function createEngine({
905
900
  bookMap.set(n.parent, list);
906
901
  }
907
902
  }
908
- for (const [bookId, chapters] of bookMap.entries()) {
909
- chapters.sort((a, b) => {
910
- const cA = a.meta?.chapter || 0;
911
- const cB = b.meta?.chapter || 0;
912
- return cA - cB;
913
- });
903
+ for (const chapters of bookMap.values()) {
904
+ chapters.sort((a, b) => (a.meta?.chapter || 0) - (b.meta?.chapter || 0));
914
905
  if (chapters.length < 2) continue;
915
- const points = [];
916
- for (const c of chapters) {
917
- const x = c.meta?.x ?? 0;
918
- const y = c.meta?.y ?? 0;
919
- const z = c.meta?.z ?? 0;
920
- points.push(new THREE4__namespace.Vector3(x, y, z));
921
- }
922
- const geo = new THREE4__namespace.BufferGeometry().setFromPoints(points);
923
- const line = new THREE4__namespace.Line(geo, lineMat);
924
- root.add(line);
925
- lineByBookId.set(bookId, line);
926
- }
927
- resize();
928
- }
929
- function applyFocus(targetId, animate = true) {
930
- if (!targetId) {
931
- for (const [id, mesh] of meshById.entries()) {
932
- mesh.traverse((obj) => {
933
- if (obj.material) obj.material.opacity = 1;
934
- });
935
- mesh.userData.interactive = true;
936
- }
937
- for (const line of lineByBookId.values()) {
938
- line.visible = true;
939
- line.material.opacity = 0.3;
940
- }
941
- return;
942
- }
943
- const childrenMap = /* @__PURE__ */ new Map();
944
- for (const n of nodeById.values()) {
945
- if (n.parent) {
946
- const list = childrenMap.get(n.parent) ?? [];
947
- list.push(n.id);
948
- childrenMap.set(n.parent, list);
906
+ for (let i = 0; i < chapters.length - 1; i++) {
907
+ const c1 = chapters[i];
908
+ const c2 = chapters[i + 1];
909
+ if (!c1 || !c2) continue;
910
+ const p1 = getPosition(c1);
911
+ const p2 = getPosition(c2);
912
+ linePoints.push(p1.x, p1.y, p1.z);
913
+ linePoints.push(p2.x, p2.y, p2.z);
949
914
  }
950
915
  }
951
- const activeIds = /* @__PURE__ */ new Set();
952
- const queue = [targetId];
953
- activeIds.add(targetId);
954
- while (queue.length > 0) {
955
- const curr = queue.pop();
956
- const kids = childrenMap.get(curr);
957
- if (kids) {
958
- for (const k of kids) {
959
- activeIds.add(k);
960
- queue.push(k);
961
- }
962
- }
963
- }
964
- for (const [id, mesh] of meshById.entries()) {
965
- const isActive = activeIds.has(id);
966
- const opacity = isActive ? 1 : 0.1;
967
- mesh.traverse((obj) => {
968
- if (obj.material) obj.material.opacity = opacity;
916
+ if (linePoints.length > 0) {
917
+ const lineGeo = new THREE4__namespace.BufferGeometry();
918
+ lineGeo.setAttribute("position", new THREE4__namespace.Float32BufferAttribute(linePoints, 3));
919
+ const lineMat = createSmartMaterial({
920
+ uniforms: { color: { value: new THREE4__namespace.Color(4478310) } },
921
+ vertexShaderBody: `uniform vec3 color; varying vec3 vColor; void main() { vColor = color; vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_Position = smartProject(mvPosition); vScreenPos = gl_Position.xy / gl_Position.w; }`,
922
+ fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.2 * alphaMask); }`,
923
+ transparent: true,
924
+ depthWrite: false,
925
+ blending: THREE4__namespace.AdditiveBlending
969
926
  });
970
- mesh.userData.interactive = isActive;
971
- }
972
- for (const [bookId, line] of lineByBookId.entries()) {
973
- const isActive = activeIds.has(bookId);
974
- line.material.opacity = isActive ? 0.3 : 0.05;
927
+ constellationLines = new THREE4__namespace.LineSegments(lineGeo, lineMat);
928
+ constellationLines.frustumCulled = false;
929
+ root.add(constellationLines);
975
930
  }
976
- if (animate) {
977
- const targetMesh = meshById.get(targetId);
978
- if (targetMesh) {
979
- animateFocusTo(targetMesh);
980
- } else {
981
- const sum = new THREE4__namespace.Vector3();
982
- let count = 0;
983
- for (const id of activeIds) {
984
- const mesh = meshById.get(id);
985
- if (mesh) {
986
- sum.add(mesh.getWorldPosition(new THREE4__namespace.Vector3()));
987
- count++;
988
- }
989
- }
990
- if (count > 0) {
991
- const centroid = sum.divideScalar(count);
992
- animateFocusTo(centroid);
931
+ const boundaries = laidOut.meta?.divisionBoundaries ?? [];
932
+ if (boundaries.length > 0) {
933
+ const boundaryMat = createSmartMaterial({
934
+ uniforms: { color: { value: new THREE4__namespace.Color(5601177) } },
935
+ vertexShaderBody: `uniform vec3 color; varying vec3 vColor; void main() { vColor = color; vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_Position = smartProject(mvPosition); vScreenPos = gl_Position.xy / gl_Position.w; }`,
936
+ fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.10 * alphaMask); }`,
937
+ transparent: true,
938
+ depthWrite: false,
939
+ blending: THREE4__namespace.AdditiveBlending
940
+ });
941
+ const boundaryGeo = new THREE4__namespace.BufferGeometry();
942
+ const bPoints = [];
943
+ boundaries.forEach((angle) => {
944
+ const steps = 32;
945
+ for (let i = 0; i < steps; i++) {
946
+ const t1 = i / steps;
947
+ const t2 = (i + 1) / steps;
948
+ const p1 = getBoundaryPoint(angle, t1, layoutCfg.radius);
949
+ const p2 = getBoundaryPoint(angle, t2, layoutCfg.radius);
950
+ bPoints.push(p1.x, p1.y, p1.z);
951
+ bPoints.push(p2.x, p2.y, p2.z);
993
952
  }
994
- }
953
+ });
954
+ boundaryGeo.setAttribute("position", new THREE4__namespace.Float32BufferAttribute(bPoints, 3));
955
+ const boundaryLines = new THREE4__namespace.LineSegments(boundaryGeo, boundaryMat);
956
+ boundaryLines.frustumCulled = false;
957
+ root.add(boundaryLines);
995
958
  }
959
+ resize();
996
960
  }
997
961
  let lastData = void 0;
998
962
  let lastAdapter = void 0;
@@ -1019,268 +983,370 @@ function createEngine({
1019
983
  }
1020
984
  if (shouldRebuild && model) {
1021
985
  buildFromModel(model, cfg);
1022
- } else if (cfg.arrangement) {
1023
- for (const [id, val] of Object.entries(cfg.arrangement)) {
1024
- const mesh = meshById.get(id);
1025
- if (mesh) {
1026
- mesh.position.set(val.position[0], val.position[1], val.position[2]);
1027
- }
1028
- }
1029
- }
1030
- if (cfg.focus?.nodeId) {
1031
- applyFocus(cfg.focus.nodeId, cfg.focus.animate);
1032
- } else {
1033
- applyFocus(void 0, false);
986
+ } else if (cfg.arrangement && starPoints) {
987
+ if (lastModel) buildFromModel(lastModel, cfg);
1034
988
  }
1035
- updateDragControls(!!cfg.editable);
1036
989
  }
1037
990
  function setHandlers(next) {
1038
991
  handlers = next;
1039
992
  }
1040
- function pick(ev) {
1041
- const rect = renderer.domElement.getBoundingClientRect();
1042
- pointer.x = (ev.clientX - rect.left) / rect.width * 2 - 1;
1043
- pointer.y = -((ev.clientY - rect.top) / rect.height * 2 - 1);
1044
- raycaster.setFromCamera(pointer, camera);
1045
- const hits = raycaster.intersectObjects(root.children, true);
1046
- const hit = hits.find(
1047
- (h) => (h.object.type === "Mesh" || h.object.type === "Sprite") && h.object.userData.interactive !== false
1048
- );
1049
- const id = hit?.object?.userData?.id;
1050
- return id ? nodeById.get(id) : void 0;
1051
- }
1052
- function onPointerMove(ev) {
1053
- const node = pick(ev);
1054
- const nextId = node?.id ?? null;
1055
- if (nextId !== hoveredId) {
1056
- if (hoveredId) {
1057
- const prevMesh = meshById.get(hoveredId);
1058
- const prevNode = nodeById.get(hoveredId);
1059
- if (prevMesh && prevNode && prevNode.level === 3) {
1060
- const label = prevMesh.children.find((c) => c instanceof THREE4__namespace.Sprite);
1061
- if (label) label.visible = false;
1062
- }
1063
- }
1064
- hoveredId = nextId;
1065
- if (nextId) {
1066
- const mesh = meshById.get(nextId);
1067
- const n = nodeById.get(nextId);
1068
- if (mesh && n && n.level === 3) {
1069
- const label = mesh.children.find((c) => c instanceof THREE4__namespace.Sprite);
1070
- if (label) {
1071
- label.visible = true;
1072
- label.position.set(0, 3, 0);
1073
- }
1074
- if (n.parent) {
1075
- const line = lineByBookId.get(n.parent);
1076
- if (line) line.material.opacity = 0.8;
993
+ function getFullArrangement() {
994
+ const arr = {};
995
+ if (starPoints && starPoints.geometry.attributes.position) {
996
+ const positions = starPoints.geometry.attributes.position.array;
997
+ for (let i = 0; i < starIndexToId.length; i++) {
998
+ const id = starIndexToId[i];
999
+ if (id) {
1000
+ const x = positions[i * 3] ?? 0;
1001
+ const y = positions[i * 3 + 1] ?? 0;
1002
+ const z = positions[i * 3 + 2] ?? 0;
1003
+ if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z)) {
1004
+ arr[id] = { position: [x, y, z] };
1077
1005
  }
1078
1006
  }
1079
1007
  }
1080
- handlers.onHover?.(node);
1081
1008
  }
1009
+ for (const item of dynamicLabels) {
1010
+ arr[item.node.id] = { position: [item.obj.position.x, item.obj.position.y, item.obj.position.z] };
1011
+ }
1012
+ return arr;
1082
1013
  }
1083
- function onPointerDown() {
1084
- isDragging = false;
1085
- }
1086
- function onChange() {
1087
- isDragging = true;
1014
+ function pick(ev) {
1015
+ const rect = renderer.domElement.getBoundingClientRect();
1016
+ const mX = ev.clientX - rect.left;
1017
+ const mY = ev.clientY - rect.top;
1018
+ mouseNDC.x = mX / rect.width * 2 - 1;
1019
+ mouseNDC.y = -(mY / rect.height) * 2 + 1;
1020
+ let closestLabel = null;
1021
+ let minLabelDist = 40;
1022
+ const uScale = globalUniforms.uScale.value;
1023
+ const uAspect = camera.aspect;
1024
+ const w = rect.width;
1025
+ const h = rect.height;
1026
+ for (const item of dynamicLabels) {
1027
+ if (!item.obj.visible) continue;
1028
+ const pWorld = item.obj.position;
1029
+ const pProj = smartProjectJS(pWorld);
1030
+ const xNDC = pProj.x * uScale / uAspect;
1031
+ const yNDC = pProj.y * uScale;
1032
+ const sX = (xNDC * 0.5 + 0.5) * w;
1033
+ const sY = (-yNDC * 0.5 + 0.5) * h;
1034
+ const dx = mX - sX;
1035
+ const dy = mY - sY;
1036
+ const d = Math.sqrt(dx * dx + dy * dy);
1037
+ const isBehind = globalUniforms.uBlend.value > 0.5 && pProj.z > 0.4 || globalUniforms.uBlend.value < 0.1 && pProj.z > -0.1;
1038
+ if (!isBehind && d < minLabelDist) {
1039
+ minLabelDist = d;
1040
+ closestLabel = item;
1041
+ }
1042
+ }
1043
+ if (closestLabel) return { type: "label", node: closestLabel.node, object: closestLabel.obj, point: closestLabel.obj.position.clone(), index: void 0 };
1044
+ const worldDir = getMouseWorldVector(mX, mY, rect.width, rect.height);
1045
+ raycaster.ray.origin.set(0, 0, 0);
1046
+ raycaster.ray.direction.copy(worldDir);
1047
+ raycaster.params.Points.threshold = 5 * (state.fov / 60);
1048
+ const hits = raycaster.intersectObject(starPoints, false);
1049
+ const pointHit = hits[0];
1050
+ if (pointHit && pointHit.index !== void 0) {
1051
+ const id = starIndexToId[pointHit.index];
1052
+ if (id) {
1053
+ const node = nodeById.get(id);
1054
+ if (node) return { type: "star", node, index: pointHit.index, point: pointHit.point, object: void 0 };
1055
+ }
1056
+ }
1057
+ return void 0;
1088
1058
  }
1089
- const onWheelFov = (ev) => {
1090
- ev.preventDefault();
1091
- const speed = (ev.ctrlKey ? 0.15 : 1) * (env.fovWheelSensitivity);
1092
- const maxFov = Math.min(env.maxFov, zoomOutLimitFov) ;
1093
- targetFov = THREE4__namespace.MathUtils.clamp(
1094
- targetFov + ev.deltaY * speed,
1095
- env.minFov,
1096
- maxFov
1097
- );
1098
- };
1099
- const onDblClick = (ev) => {
1100
- const node = pick(ev);
1101
- if (node) {
1102
- const mesh = meshById.get(node.id);
1103
- if (mesh) {
1104
- animateFocusTo(mesh);
1059
+ function onMouseDown(e) {
1060
+ state.lastMouseX = e.clientX;
1061
+ state.lastMouseY = e.clientY;
1062
+ if (currentConfig?.editable) {
1063
+ const hit = pick(e);
1064
+ if (hit) {
1065
+ state.dragMode = "node";
1066
+ state.draggedNodeId = hit.node.id;
1067
+ state.draggedDist = hit.point.length();
1068
+ document.body.style.cursor = "crosshair";
1069
+ if (hit.type === "star") {
1070
+ state.draggedStarIndex = hit.index ?? -1;
1071
+ state.draggedGroup = null;
1072
+ } else if (hit.type === "label") {
1073
+ const bookId = hit.node.id;
1074
+ const children = [];
1075
+ if (starPoints && starPoints.geometry.attributes.position) {
1076
+ const positions = starPoints.geometry.attributes.position.array;
1077
+ for (let i = 0; i < starIndexToId.length; i++) {
1078
+ const starId = starIndexToId[i];
1079
+ if (starId) {
1080
+ const starNode = nodeById.get(starId);
1081
+ if (starNode && starNode.parent === bookId) {
1082
+ children.push({ index: i, initialPos: new THREE4__namespace.Vector3(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]) });
1083
+ }
1084
+ }
1085
+ }
1086
+ }
1087
+ state.draggedGroup = { labelInitialPos: hit.object.position.clone(), children };
1088
+ state.draggedStarIndex = -1;
1089
+ }
1105
1090
  return;
1106
1091
  }
1107
1092
  }
1108
- targetFov = env.defaultFov;
1109
- };
1110
- let focusAnimRaf = 0;
1111
- function cancelFocusAnim() {
1112
- if (focusAnimRaf) cancelAnimationFrame(focusAnimRaf);
1113
- focusAnimRaf = 0;
1114
- }
1115
- function getControlsAnglesSafe() {
1116
- const getAz = controls.getAzimuthalAngle?.bind(controls);
1117
- const getPol = controls.getPolarAngle?.bind(controls);
1118
- return {
1119
- azimuth: typeof getAz === "function" ? getAz() : 0,
1120
- polar: typeof getPol === "function" ? getPol() : Math.PI / 4
1121
- };
1093
+ state.dragMode = "camera";
1094
+ state.isDragging = true;
1095
+ state.velocityX = 0;
1096
+ state.velocityY = 0;
1097
+ document.body.style.cursor = "grabbing";
1122
1098
  }
1123
- function setControlsAnglesSafe(azimuth, polar) {
1124
- const setAz = controls.setAzimuthalAngle?.bind(controls);
1125
- const setPol = controls.setPolarAngle?.bind(controls);
1126
- if (typeof setAz === "function" && typeof setPol === "function") {
1127
- setAz(azimuth);
1128
- setPol(polar);
1129
- controls.update();
1130
- return;
1099
+ function onMouseMove(e) {
1100
+ const rect = renderer.domElement.getBoundingClientRect();
1101
+ const mX = e.clientX - rect.left;
1102
+ const mY = e.clientY - rect.top;
1103
+ mouseNDC.x = mX / rect.width * 2 - 1;
1104
+ mouseNDC.y = -(mY / rect.height) * 2 + 1;
1105
+ isMouseInWindow = true;
1106
+ if (state.dragMode === "node") {
1107
+ const worldDir = getMouseWorldVector(mX, mY, rect.width, rect.height);
1108
+ const newPos = worldDir.multiplyScalar(state.draggedDist);
1109
+ if (state.draggedStarIndex !== -1 && starPoints) {
1110
+ const idx = state.draggedStarIndex;
1111
+ const attr = starPoints.geometry.attributes.position;
1112
+ attr.setXYZ(idx, newPos.x, newPos.y, newPos.z);
1113
+ attr.needsUpdate = true;
1114
+ } else if (state.draggedGroup && state.draggedNodeId) {
1115
+ const group = state.draggedGroup;
1116
+ const item = dynamicLabels.find((l) => l.node.id === state.draggedNodeId);
1117
+ if (item) item.obj.position.copy(newPos);
1118
+ const vStart = group.labelInitialPos.clone().normalize();
1119
+ const vEnd = newPos.clone().normalize();
1120
+ const q = new THREE4__namespace.Quaternion().setFromUnitVectors(vStart, vEnd);
1121
+ if (starPoints && group.children.length > 0) {
1122
+ const attr = starPoints.geometry.attributes.position;
1123
+ const tempVec = new THREE4__namespace.Vector3();
1124
+ for (const child of group.children) {
1125
+ tempVec.copy(child.initialPos).applyQuaternion(q);
1126
+ attr.setXYZ(child.index, tempVec.x, tempVec.y, tempVec.z);
1127
+ }
1128
+ attr.needsUpdate = true;
1129
+ }
1130
+ }
1131
+ } else if (state.dragMode === "camera") {
1132
+ const deltaX = e.clientX - state.lastMouseX;
1133
+ const deltaY = e.clientY - state.lastMouseY;
1134
+ state.lastMouseX = e.clientX;
1135
+ state.lastMouseY = e.clientY;
1136
+ const speedScale = state.fov / ENGINE_CONFIG.defaultFov;
1137
+ state.targetLon += deltaX * ENGINE_CONFIG.dragSpeed * speedScale;
1138
+ state.targetLat += deltaY * ENGINE_CONFIG.dragSpeed * speedScale;
1139
+ state.targetLat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.targetLat));
1140
+ state.velocityX = deltaX * ENGINE_CONFIG.dragSpeed * speedScale;
1141
+ state.velocityY = deltaY * ENGINE_CONFIG.dragSpeed * speedScale;
1142
+ state.lon = state.targetLon;
1143
+ state.lat = state.targetLat;
1144
+ } else {
1145
+ const hit = pick(e);
1146
+ if (hit?.node.id !== handlers._lastHoverId) {
1147
+ handlers._lastHoverId = hit?.node.id;
1148
+ handlers.onHover?.(hit?.node);
1149
+ }
1150
+ document.body.style.cursor = hit ? currentConfig?.editable ? "crosshair" : "pointer" : "default";
1131
1151
  }
1132
- const dir = new THREE4__namespace.Vector3();
1133
- dir.setFromSphericalCoords(1, polar, azimuth);
1134
- const lookAt2 = dir.clone().multiplyScalar(10);
1135
- camera.lookAt(lookAt2);
1136
1152
  }
1137
- function aimAtWorldPoint(worldPoint) {
1138
- const dir = worldPoint.clone().normalize().negate();
1139
- const spherical = new THREE4__namespace.Spherical().setFromVector3(dir);
1140
- let targetPolar = spherical.phi;
1141
- let targetAz = spherical.theta;
1142
- targetPolar = THREE4__namespace.MathUtils.clamp(targetPolar, controls.minPolarAngle, controls.maxPolarAngle);
1143
- return { targetAz, targetPolar };
1144
- }
1145
- function easeInOutCubic(t) {
1146
- return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
1153
+ function onMouseUp(e) {
1154
+ if (state.dragMode === "node") {
1155
+ const fullArr = getFullArrangement();
1156
+ handlers.onArrangementChange?.(fullArr);
1157
+ state.dragMode = "none";
1158
+ state.draggedNodeId = null;
1159
+ state.draggedStarIndex = -1;
1160
+ state.draggedGroup = null;
1161
+ document.body.style.cursor = "default";
1162
+ } else if (state.dragMode === "camera") {
1163
+ state.isDragging = false;
1164
+ state.dragMode = "none";
1165
+ document.body.style.cursor = "default";
1166
+ } else {
1167
+ const hit = pick(e);
1168
+ if (hit) handlers.onSelect?.(hit.node);
1169
+ }
1147
1170
  }
1148
- function animateFocusTo(target) {
1149
- cancelFocusAnim();
1150
- const { azimuth: startAz, polar: startPolar } = getControlsAnglesSafe();
1151
- const startFov = camera.fov;
1152
- const targetPos = target instanceof THREE4__namespace.Object3D ? target.getWorldPosition(new THREE4__namespace.Vector3()) : target;
1153
- const { targetAz, targetPolar } = aimAtWorldPoint(targetPos);
1154
- const endFov = THREE4__namespace.MathUtils.clamp(env.focusZoomFov, env.minFov, env.maxFov);
1155
- const start2 = performance.now();
1156
- const dur = Math.max(120, env.focusDurationMs);
1157
- const tick = (now) => {
1158
- const t = THREE4__namespace.MathUtils.clamp((now - start2) / dur, 0, 1);
1159
- const k = easeInOutCubic(t);
1160
- let dAz = targetAz - startAz;
1161
- dAz = (dAz + Math.PI) % (Math.PI * 2) - Math.PI;
1162
- const curAz = startAz + dAz * k;
1163
- const curPolar = THREE4__namespace.MathUtils.lerp(startPolar, targetPolar, k);
1164
- setControlsAnglesSafe(curAz, curPolar);
1165
- camera.fov = THREE4__namespace.MathUtils.lerp(startFov, endFov, k);
1166
- camera.updateProjectionMatrix();
1167
- if (t < 1) {
1168
- focusAnimRaf = requestAnimationFrame(tick);
1169
- } else {
1170
- focusAnimRaf = 0;
1171
- }
1172
- };
1173
- focusAnimRaf = requestAnimationFrame(tick);
1171
+ function onWheel(e) {
1172
+ e.preventDefault();
1173
+ const aspect = container.clientWidth / container.clientHeight;
1174
+ renderer.domElement.getBoundingClientRect();
1175
+ const vBefore = getMouseViewVector(state.fov, aspect);
1176
+ const zoomSpeed = 1e-3 * state.fov;
1177
+ state.fov += e.deltaY * zoomSpeed;
1178
+ state.fov = Math.max(ENGINE_CONFIG.minFov, Math.min(ENGINE_CONFIG.maxFov, state.fov));
1179
+ updateUniforms();
1180
+ const vAfter = getMouseViewVector(state.fov, aspect);
1181
+ const quaternion = new THREE4__namespace.Quaternion().setFromUnitVectors(vAfter, vBefore);
1182
+ const y = Math.sin(state.lat);
1183
+ const r = Math.cos(state.lat);
1184
+ const x = r * Math.sin(state.lon);
1185
+ const z = -r * Math.cos(state.lon);
1186
+ const currentLook = new THREE4__namespace.Vector3(x, y, z);
1187
+ const camForward = currentLook.clone().normalize();
1188
+ const camUp = camera.up.clone();
1189
+ const camRight = new THREE4__namespace.Vector3().crossVectors(camForward, camUp).normalize();
1190
+ const camUpOrtho = new THREE4__namespace.Vector3().crossVectors(camRight, camForward).normalize();
1191
+ const mat = new THREE4__namespace.Matrix4().makeBasis(camRight, camUpOrtho, camForward.clone().negate());
1192
+ const qOld = new THREE4__namespace.Quaternion().setFromRotationMatrix(mat);
1193
+ const qNew = qOld.clone().multiply(quaternion);
1194
+ const newForward = new THREE4__namespace.Vector3(0, 0, -1).applyQuaternion(qNew);
1195
+ state.lat = Math.asin(Math.max(-0.999, Math.min(0.999, newForward.y)));
1196
+ state.lon = Math.atan2(newForward.x, -newForward.z);
1197
+ const newUp = new THREE4__namespace.Vector3(0, 1, 0).applyQuaternion(qNew);
1198
+ camera.up.copy(newUp);
1199
+ if (e.deltaY > 0 && state.fov > ENGINE_CONFIG.zenithStartFov) {
1200
+ const range = ENGINE_CONFIG.maxFov - ENGINE_CONFIG.zenithStartFov;
1201
+ let t = (state.fov - ENGINE_CONFIG.zenithStartFov) / range;
1202
+ t = Math.max(0, Math.min(1, t));
1203
+ const bias = ENGINE_CONFIG.zenithStrength * t;
1204
+ const zenithLat = Math.PI / 2 - 1e-3;
1205
+ state.lat = mix(state.lat, zenithLat, bias);
1206
+ }
1207
+ state.targetLat = state.lat;
1208
+ state.targetLon = state.lon;
1174
1209
  }
1175
- function onPointerUp(ev) {
1176
- if (isDragging) return;
1177
- const node = pick(ev);
1178
- if (!node) return;
1179
- handlers.onSelect?.(node);
1210
+ function resize() {
1211
+ const w = container.clientWidth || 1;
1212
+ const h = container.clientHeight || 1;
1213
+ renderer.setSize(w, h, false);
1214
+ camera.aspect = w / h;
1215
+ updateUniforms();
1180
1216
  }
1181
1217
  function start() {
1182
1218
  if (running) return;
1183
1219
  running = true;
1184
1220
  resize();
1185
1221
  window.addEventListener("resize", resize);
1186
- renderer.domElement.addEventListener("pointermove", onPointerMove);
1187
- renderer.domElement.addEventListener("pointerdown", onPointerDown);
1188
- renderer.domElement.addEventListener("pointerup", onPointerUp);
1189
- renderer.domElement.addEventListener("wheel", onWheelFov, { passive: false });
1190
- renderer.domElement.addEventListener("dblclick", onDblClick);
1191
- controls.addEventListener("change", onChange);
1192
- lastFrameMs = performance.now();
1193
- const tick = () => {
1194
- raf = requestAnimationFrame(tick);
1195
- const now = performance.now();
1196
- const dt = Math.min(0.05, Math.max(0, (now - lastFrameMs) / 1e3));
1197
- lastFrameMs = now;
1198
- const lerpK = 1 - Math.pow(1 - (env.zoomLerp), dt * 60);
1199
- const prevFov = camera.fov;
1200
- camera.fov = THREE4__namespace.MathUtils.lerp(camera.fov, targetFov, lerpK);
1201
- if (Math.abs(camera.fov - prevFov) > 1e-4) camera.updateProjectionMatrix();
1202
- controls.rotateSpeed = camera.fov / (env.defaultFov);
1203
- {
1204
- backdropGroup.rotation.y += SIDEREAL_RATE * (env.timeScale) * dt;
1205
- }
1206
- const fovNorm = (env.maxFov - camera.fov) / Math.max(1e-6, env.maxFov - env.minFov);
1207
- const fovN = THREE4__namespace.MathUtils.clamp(fovNorm, 0, 1);
1208
- const overlayFade = THREE4__namespace.MathUtils.clamp(1 - fovN * 1.15, 0, 1);
1209
- const minZoomFov = 15;
1210
- const scaleFactor = THREE4__namespace.MathUtils.clamp(
1211
- 1 + (camera.fov - minZoomFov) * 0.05,
1212
- 0.85,
1213
- 6
1214
- );
1215
- const cameraDir = new THREE4__namespace.Vector3();
1216
- camera.getWorldDirection(cameraDir);
1217
- const objPos = new THREE4__namespace.Vector3();
1218
- const objDir = new THREE4__namespace.Vector3();
1219
- for (const item of dynamicObjects) {
1220
- item.obj.scale.copy(item.initialScale).multiplyScalar(scaleFactor);
1221
- if (item.type === "label") {
1222
- const sprite = item.obj;
1223
- sprite.getWorldPosition(objPos);
1224
- objDir.subVectors(objPos, camera.position).normalize();
1225
- const dot = cameraDir.dot(objDir);
1226
- const fullVisibleDot = 0.96;
1227
- const invisibleDot = 0.88;
1228
- let opacity = 0;
1229
- if (dot >= fullVisibleDot) {
1230
- opacity = 1;
1231
- } else if (dot > invisibleDot) {
1232
- opacity = (dot - invisibleDot) / (fullVisibleDot - invisibleDot);
1233
- }
1234
- const finalOpacity = opacity * overlayFade;
1235
- sprite.material.opacity = finalOpacity;
1236
- sprite.visible = finalOpacity > 0.01;
1237
- const bookId = nodeById.get(item.obj.userData.id)?.id;
1238
- if (bookId) {
1239
- const line = lineByBookId.get(bookId);
1240
- if (line) {
1241
- line.material.opacity = (0.05 + opacity * 0.45) * overlayFade;
1242
- }
1243
- }
1244
- }
1222
+ const el = renderer.domElement;
1223
+ el.addEventListener("mousedown", onMouseDown);
1224
+ window.addEventListener("mousemove", onMouseMove);
1225
+ window.addEventListener("mouseup", onMouseUp);
1226
+ el.addEventListener("wheel", onWheel, { passive: false });
1227
+ el.addEventListener("mouseenter", () => {
1228
+ isMouseInWindow = true;
1229
+ });
1230
+ el.addEventListener("mouseleave", () => {
1231
+ isMouseInWindow = false;
1232
+ });
1233
+ raf = requestAnimationFrame(tick);
1234
+ }
1235
+ function tick() {
1236
+ if (!running) return;
1237
+ raf = requestAnimationFrame(tick);
1238
+ if (!state.isDragging && isMouseInWindow) {
1239
+ const t = ENGINE_CONFIG.edgePanThreshold;
1240
+ const speedBase = ENGINE_CONFIG.edgePanMaxSpeed * (state.fov / ENGINE_CONFIG.defaultFov);
1241
+ let panX = 0;
1242
+ let panY = 0;
1243
+ if (mouseNDC.x < -1 + t) {
1244
+ const s = (-1 + t - mouseNDC.x) / t;
1245
+ panX = -s * s * speedBase;
1246
+ } else if (mouseNDC.x > 1 - t) {
1247
+ const s = (mouseNDC.x - (1 - t)) / t;
1248
+ panX = s * s * speedBase;
1249
+ }
1250
+ if (mouseNDC.y < -1 + t) {
1251
+ const s = (-1 + t - mouseNDC.y) / t;
1252
+ panY = -s * s * speedBase;
1253
+ } else if (mouseNDC.y > 1 - t) {
1254
+ const s = (mouseNDC.y - (1 - t)) / t;
1255
+ panY = s * s * speedBase;
1256
+ }
1257
+ if (Math.abs(panX) > 0 || Math.abs(panY) > 0) {
1258
+ state.lon += panX;
1259
+ state.lat += panY;
1260
+ state.targetLon = state.lon;
1261
+ state.targetLat = state.lat;
1262
+ } else {
1263
+ state.lon += state.velocityX;
1264
+ state.lat += state.velocityY;
1265
+ state.velocityX *= ENGINE_CONFIG.inertiaDamping;
1266
+ state.velocityY *= ENGINE_CONFIG.inertiaDamping;
1267
+ if (Math.abs(state.velocityX) < 1e-6) state.velocityX = 0;
1268
+ if (Math.abs(state.velocityY) < 1e-6) state.velocityY = 0;
1269
+ }
1270
+ } else if (!state.isDragging) {
1271
+ state.lon += state.velocityX;
1272
+ state.lat += state.velocityY;
1273
+ state.velocityX *= ENGINE_CONFIG.inertiaDamping;
1274
+ state.velocityY *= ENGINE_CONFIG.inertiaDamping;
1275
+ }
1276
+ state.lat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.lat));
1277
+ const y = Math.sin(state.lat);
1278
+ const r = Math.cos(state.lat);
1279
+ const x = r * Math.sin(state.lon);
1280
+ const z = -r * Math.cos(state.lon);
1281
+ const target = new THREE4__namespace.Vector3(x, y, z);
1282
+ const idealUp = new THREE4__namespace.Vector3(-Math.sin(state.lat) * Math.sin(state.lon), Math.cos(state.lat), Math.sin(state.lat) * Math.cos(state.lon)).normalize();
1283
+ camera.up.lerp(idealUp, ENGINE_CONFIG.horizonLockStrength);
1284
+ camera.up.normalize();
1285
+ camera.lookAt(target);
1286
+ updateUniforms();
1287
+ const cameraDir = new THREE4__namespace.Vector3();
1288
+ camera.getWorldDirection(cameraDir);
1289
+ const objPos = new THREE4__namespace.Vector3();
1290
+ const objDir = new THREE4__namespace.Vector3();
1291
+ const SHOW_LABELS_FOV = 60;
1292
+ for (const item of dynamicLabels) {
1293
+ const uniforms = item.obj.material.uniforms;
1294
+ let targetAlpha = 0;
1295
+ if (state.fov < SHOW_LABELS_FOV) {
1296
+ item.obj.getWorldPosition(objPos);
1297
+ objDir.subVectors(objPos, camera.position).normalize();
1298
+ const dot = cameraDir.dot(objDir);
1299
+ const fullVisibleDot = 0.98;
1300
+ const invisibleDot = 0.9;
1301
+ let gazeOpacity = 0;
1302
+ if (dot >= fullVisibleDot) gazeOpacity = 1;
1303
+ else if (dot > invisibleDot) gazeOpacity = (dot - invisibleDot) / (fullVisibleDot - invisibleDot);
1304
+ const zoomFactor = 1 - THREE4__namespace.MathUtils.smoothstep(40, SHOW_LABELS_FOV, state.fov);
1305
+ targetAlpha = gazeOpacity * zoomFactor;
1306
+ }
1307
+ if (uniforms.uAlpha) {
1308
+ uniforms.uAlpha.value = THREE4__namespace.MathUtils.lerp(uniforms.uAlpha.value, targetAlpha, 0.1);
1309
+ item.obj.visible = uniforms.uAlpha.value > 0.01;
1245
1310
  }
1246
- controls.update();
1247
- renderer.render(scene, camera);
1248
- };
1249
- tick();
1311
+ }
1312
+ renderer.render(scene, camera);
1250
1313
  }
1251
1314
  function stop() {
1252
1315
  running = false;
1253
1316
  cancelAnimationFrame(raf);
1254
- cancelFocusAnim();
1255
1317
  window.removeEventListener("resize", resize);
1256
- renderer.domElement.removeEventListener("pointermove", onPointerMove);
1257
- renderer.domElement.removeEventListener("pointerdown", onPointerDown);
1258
- renderer.domElement.removeEventListener("pointerup", onPointerUp);
1259
- renderer.domElement.removeEventListener("wheel", onWheelFov);
1260
- renderer.domElement.removeEventListener("dblclick", onDblClick);
1261
- controls.removeEventListener("change", onChange);
1318
+ const el = renderer.domElement;
1319
+ el.removeEventListener("mousedown", onMouseDown);
1320
+ window.removeEventListener("mousemove", onMouseMove);
1321
+ window.removeEventListener("mouseup", onMouseUp);
1322
+ el.removeEventListener("wheel", onWheel);
1262
1323
  }
1263
1324
  function dispose() {
1264
1325
  stop();
1265
- clearRoot();
1266
- for (const child of [...groundGroup.children]) {
1267
- groundGroup.remove(child);
1268
- disposeObject(child);
1269
- }
1270
- for (const child of [...backdropGroup.children]) {
1271
- backdropGroup.remove(child);
1272
- disposeObject(child);
1273
- }
1274
- controls.dispose();
1275
1326
  renderer.dispose();
1276
1327
  renderer.domElement.remove();
1277
1328
  }
1278
1329
  return { setConfig, start, stop, dispose, setHandlers, getFullArrangement };
1279
1330
  }
1331
+ var ENGINE_CONFIG;
1280
1332
  var init_createEngine = __esm({
1281
1333
  "src/engine/createEngine.ts"() {
1282
1334
  init_layout();
1283
1335
  init_materials();
1336
+ ENGINE_CONFIG = {
1337
+ minFov: 10,
1338
+ maxFov: 165,
1339
+ defaultFov: 80,
1340
+ dragSpeed: 125e-5,
1341
+ inertiaDamping: 0.92,
1342
+ blendStart: 60,
1343
+ blendEnd: 165,
1344
+ zenithStartFov: 110,
1345
+ zenithStrength: 0.02,
1346
+ horizonLockStrength: 0.05,
1347
+ edgePanThreshold: 0.15,
1348
+ edgePanMaxSpeed: 0.02
1349
+ };
1284
1350
  }
1285
1351
  });
1286
1352
  var StarMap = react.forwardRef(