@project-skymap/library 0.2.0 → 0.2.1
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 +839 -773
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +839 -773
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
454
|
-
camera.
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
|
523
|
+
function createGround() {
|
|
490
524
|
groundGroup.clear();
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
|
516
|
-
groundGroup.add(
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
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
|
|
601
|
+
function createBackdropStars() {
|
|
561
602
|
backdropGroup.clear();
|
|
562
|
-
const
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
578
|
-
|
|
678
|
+
depthWrite: false,
|
|
679
|
+
depthTest: true
|
|
579
680
|
});
|
|
580
|
-
const
|
|
581
|
-
|
|
681
|
+
const points = new THREE4__namespace.Points(geometry, material);
|
|
682
|
+
points.frustumCulled = false;
|
|
683
|
+
backdropGroup.add(points);
|
|
582
684
|
}
|
|
583
|
-
|
|
685
|
+
createGround();
|
|
686
|
+
createAtmosphere();
|
|
687
|
+
createBackdropStars();
|
|
584
688
|
const raycaster = new THREE4__namespace.Raycaster();
|
|
585
|
-
|
|
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
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
for (const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
-
|
|
777
|
-
|
|
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
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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
|
-
|
|
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
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
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
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
916
|
-
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
const
|
|
992
|
-
|
|
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
|
-
|
|
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
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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
|
|
1124
|
-
const
|
|
1125
|
-
const
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
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
|
|
1149
|
-
|
|
1150
|
-
const
|
|
1151
|
-
|
|
1152
|
-
const
|
|
1153
|
-
const
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
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
|
|
1176
|
-
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
|
|
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
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
)
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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
|
-
|
|
1247
|
-
|
|
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
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
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(
|