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