@project-skymap/library 0.3.0 → 0.5.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 +1377 -293
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -1
- package/dist/index.d.ts +65 -1
- package/dist/index.js +1375 -293
- package/dist/index.js.map +1 -1
- package/package.json +8 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as THREE5 from 'three';
|
|
2
2
|
import { forwardRef, useRef, useImperativeHandle, useEffect } from 'react';
|
|
3
3
|
import { jsx } from 'react/jsx-runtime';
|
|
4
4
|
|
|
@@ -129,14 +129,14 @@ var init_constellations = __esm({
|
|
|
129
129
|
});
|
|
130
130
|
function lookAt(point, target, up) {
|
|
131
131
|
const zAxis = target.clone().normalize();
|
|
132
|
-
let xAxis = new
|
|
132
|
+
let xAxis = new THREE5.Vector3().crossVectors(up, zAxis);
|
|
133
133
|
if (xAxis.lengthSq() < 1e-4) {
|
|
134
|
-
xAxis = new
|
|
134
|
+
xAxis = new THREE5.Vector3().crossVectors(new THREE5.Vector3(1, 0, 0), zAxis);
|
|
135
135
|
}
|
|
136
136
|
xAxis.normalize();
|
|
137
|
-
const yAxis = new
|
|
138
|
-
const m = new
|
|
139
|
-
const v = new
|
|
137
|
+
const yAxis = new THREE5.Vector3().crossVectors(zAxis, xAxis).normalize();
|
|
138
|
+
const m = new THREE5.Matrix4().makeBasis(xAxis, yAxis, zAxis);
|
|
139
|
+
const v = new THREE5.Vector3(point.x, point.y, point.z);
|
|
140
140
|
v.applyMatrix4(m);
|
|
141
141
|
v.add(target);
|
|
142
142
|
return { x: v.x, y: v.y, z: v.z };
|
|
@@ -217,7 +217,7 @@ function computeLayoutPositions(model, layout) {
|
|
|
217
217
|
const radiusAtY = Math.sqrt(1 - y * y);
|
|
218
218
|
const x = Math.cos(midAngle) * radiusAtY;
|
|
219
219
|
const z = Math.sin(midAngle) * radiusAtY;
|
|
220
|
-
const labelPos = new
|
|
220
|
+
const labelPos = new THREE5.Vector3(x, y, z).multiplyScalar(radius);
|
|
221
221
|
uDivision.meta.x = labelPos.x;
|
|
222
222
|
uDivision.meta.y = labelPos.y;
|
|
223
223
|
uDivision.meta.z = labelPos.z;
|
|
@@ -233,7 +233,7 @@ function computeLayoutPositions(model, layout) {
|
|
|
233
233
|
const theta = startAngle + t * angleSpan;
|
|
234
234
|
const x = Math.cos(theta) * radiusAtY;
|
|
235
235
|
const z = Math.sin(theta) * radiusAtY;
|
|
236
|
-
const bookPos = new
|
|
236
|
+
const bookPos = new THREE5.Vector3(x, y, z).multiplyScalar(radius);
|
|
237
237
|
const labelPos = bookPos.clone();
|
|
238
238
|
labelPos.y += radius * 0.025;
|
|
239
239
|
labelPos.setLength(radius);
|
|
@@ -244,7 +244,7 @@ function computeLayoutPositions(model, layout) {
|
|
|
244
244
|
if (chapters.length > 0) {
|
|
245
245
|
const territoryRadius = radius * 2 / Math.sqrt(books.length * 2) * 0.7;
|
|
246
246
|
const localPoints = getConstellationLayout(bookKey, chapters.length, territoryRadius);
|
|
247
|
-
const up = new
|
|
247
|
+
const up = new THREE5.Vector3(0, 1, 0);
|
|
248
248
|
chapters.forEach((chap, idx) => {
|
|
249
249
|
const uChap = updatedNodeMap.get(chap.id);
|
|
250
250
|
const lp = localPoints[idx];
|
|
@@ -263,10 +263,10 @@ function computeLayoutPositions(model, layout) {
|
|
|
263
263
|
testaments.forEach((t) => {
|
|
264
264
|
const children = childrenMap.get(t.id) ?? [];
|
|
265
265
|
if (children.length === 0) return;
|
|
266
|
-
const centroid = new
|
|
266
|
+
const centroid = new THREE5.Vector3();
|
|
267
267
|
children.forEach((c) => {
|
|
268
268
|
const u = updatedNodeMap.get(c.id);
|
|
269
|
-
centroid.add(new
|
|
269
|
+
centroid.add(new THREE5.Vector3(u.meta.x, u.meta.y, u.meta.z));
|
|
270
270
|
});
|
|
271
271
|
centroid.divideScalar(children.length);
|
|
272
272
|
if (centroid.length() > 0.1) {
|
|
@@ -330,11 +330,18 @@ vec4 smartProject(vec4 viewPos) {
|
|
|
330
330
|
vec2 projected = vec2(k * dir.x, k * dir.y);
|
|
331
331
|
projected *= uScale;
|
|
332
332
|
projected.x /= uAspect;
|
|
333
|
-
float zMetric = -1.0 + (dist /
|
|
333
|
+
float zMetric = -1.0 + (dist / 15000.0);
|
|
334
|
+
|
|
335
|
+
// Radial Clipping: Push clipped points off-screen in their natural direction
|
|
336
|
+
// to prevent lines "darting" across the center.
|
|
337
|
+
vec2 escapeDir = (length(dir.xy) > 0.0001) ? normalize(dir.xy) : vec2(1.0, 1.0);
|
|
338
|
+
vec2 escapePos = escapeDir * 10000.0;
|
|
339
|
+
|
|
334
340
|
// Clip backward facing points in fisheye mode
|
|
335
|
-
if (uBlend > 0.5 && dir.z > 0.4) return vec4(
|
|
341
|
+
if (uBlend > 0.5 && dir.z > 0.4) return vec4(escapePos, 10.0, 1.0);
|
|
336
342
|
// Clip very close points in linear mode
|
|
337
|
-
if (uBlend < 0.1 && dir.z > -0.1) return vec4(
|
|
343
|
+
if (uBlend < 0.1 && dir.z > -0.1) return vec4(escapePos, 10.0, 1.0);
|
|
344
|
+
|
|
338
345
|
return vec4(projected, zMetric, 1.0);
|
|
339
346
|
}
|
|
340
347
|
`;
|
|
@@ -357,7 +364,7 @@ float getMaskAlpha() {
|
|
|
357
364
|
});
|
|
358
365
|
function createSmartMaterial(params) {
|
|
359
366
|
const uniforms = { ...globalUniforms, ...params.uniforms };
|
|
360
|
-
return new
|
|
367
|
+
return new THREE5.ShaderMaterial({
|
|
361
368
|
uniforms,
|
|
362
369
|
vertexShader: `
|
|
363
370
|
${BLEND_CHUNK}
|
|
@@ -371,8 +378,8 @@ function createSmartMaterial(params) {
|
|
|
371
378
|
transparent: params.transparent || false,
|
|
372
379
|
depthWrite: params.depthWrite !== void 0 ? params.depthWrite : true,
|
|
373
380
|
depthTest: params.depthTest !== void 0 ? params.depthTest : true,
|
|
374
|
-
side: params.side ||
|
|
375
|
-
blending: params.blending ||
|
|
381
|
+
side: params.side || THREE5.FrontSide,
|
|
382
|
+
blending: params.blending || THREE5.NormalBlending
|
|
376
383
|
});
|
|
377
384
|
}
|
|
378
385
|
var globalUniforms;
|
|
@@ -382,7 +389,248 @@ var init_materials = __esm({
|
|
|
382
389
|
globalUniforms = {
|
|
383
390
|
uScale: { value: 1 },
|
|
384
391
|
uAspect: { value: 1 },
|
|
385
|
-
uBlend: { value: 0 }
|
|
392
|
+
uBlend: { value: 0 },
|
|
393
|
+
uTime: { value: 0 },
|
|
394
|
+
// Atmosphere Settings
|
|
395
|
+
uAtmGlow: { value: 1 },
|
|
396
|
+
uAtmDark: { value: 0.6 },
|
|
397
|
+
uAtmExtinction: { value: 4 },
|
|
398
|
+
uAtmTwinkle: { value: 0.8 },
|
|
399
|
+
uColorHorizon: { value: new THREE5.Color(2768476) },
|
|
400
|
+
uColorZenith: { value: new THREE5.Color(132104) }
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
var ConstellationArtworkLayer;
|
|
405
|
+
var init_ConstellationArtworkLayer = __esm({
|
|
406
|
+
"src/engine/ConstellationArtworkLayer.ts"() {
|
|
407
|
+
init_materials();
|
|
408
|
+
ConstellationArtworkLayer = class {
|
|
409
|
+
root;
|
|
410
|
+
items = [];
|
|
411
|
+
textureLoader = new THREE5.TextureLoader();
|
|
412
|
+
hoveredId = null;
|
|
413
|
+
focusedId = null;
|
|
414
|
+
constructor(root) {
|
|
415
|
+
this.root = new THREE5.Group();
|
|
416
|
+
this.root.renderOrder = -1;
|
|
417
|
+
root.add(this.root);
|
|
418
|
+
}
|
|
419
|
+
getItems() {
|
|
420
|
+
return this.items;
|
|
421
|
+
}
|
|
422
|
+
setPosition(id, pos) {
|
|
423
|
+
const item = this.items.find((i) => i.config.id === id);
|
|
424
|
+
if (item) {
|
|
425
|
+
item.mesh.position.copy(pos);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
load(config, getPosition) {
|
|
429
|
+
this.clear();
|
|
430
|
+
const basePath = config.atlasBasePath.replace(/\/$/, "");
|
|
431
|
+
config.constellations.forEach((c) => {
|
|
432
|
+
let center = new THREE5.Vector3();
|
|
433
|
+
let valid = false;
|
|
434
|
+
let radius = 2e3;
|
|
435
|
+
const arrPos = getPosition(c.id);
|
|
436
|
+
if (arrPos) {
|
|
437
|
+
center.copy(arrPos);
|
|
438
|
+
valid = true;
|
|
439
|
+
if (c.anchors.length > 0) {
|
|
440
|
+
const points = [];
|
|
441
|
+
for (const anchorId of c.anchors) {
|
|
442
|
+
const p = getPosition(anchorId);
|
|
443
|
+
if (p) points.push(p);
|
|
444
|
+
}
|
|
445
|
+
if (points.length > 0) {
|
|
446
|
+
radius = points[0].length();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
} else if (c.center) {
|
|
450
|
+
center.set(c.center[0], c.center[1], c.center[2]);
|
|
451
|
+
valid = true;
|
|
452
|
+
} else if (c.anchors.length > 0) {
|
|
453
|
+
const points = [];
|
|
454
|
+
for (const anchorId of c.anchors) {
|
|
455
|
+
const p = getPosition(anchorId);
|
|
456
|
+
if (p) points.push(p);
|
|
457
|
+
}
|
|
458
|
+
if (points.length > 0) {
|
|
459
|
+
for (const p of points) center.add(p);
|
|
460
|
+
center.divideScalar(points.length);
|
|
461
|
+
const len = center.length();
|
|
462
|
+
if (len > 1e-3) {
|
|
463
|
+
radius = points[0].length();
|
|
464
|
+
center.normalize().multiplyScalar(radius);
|
|
465
|
+
}
|
|
466
|
+
valid = true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (!valid) return;
|
|
470
|
+
const normal = center.clone().normalize().negate();
|
|
471
|
+
const upVec = center.clone().normalize();
|
|
472
|
+
let right = new THREE5.Vector3(1, 0, 0);
|
|
473
|
+
if (c.anchors.length >= 2) {
|
|
474
|
+
const p0 = getPosition(c.anchors[0]);
|
|
475
|
+
const p1 = getPosition(c.anchors[1]);
|
|
476
|
+
if (p0 && p1) {
|
|
477
|
+
const diff = new THREE5.Vector3().subVectors(p1, p0);
|
|
478
|
+
right.copy(diff).sub(upVec.clone().multiplyScalar(diff.dot(upVec))).normalize();
|
|
479
|
+
}
|
|
480
|
+
} else {
|
|
481
|
+
if (Math.abs(upVec.y) > 0.9) right.set(1, 0, 0).cross(upVec).normalize();
|
|
482
|
+
else right.set(0, 1, 0).cross(upVec).normalize();
|
|
483
|
+
}
|
|
484
|
+
const top = new THREE5.Vector3().crossVectors(upVec, right).normalize();
|
|
485
|
+
right.crossVectors(top, upVec).normalize();
|
|
486
|
+
new THREE5.Matrix4().makeBasis(right, top, normal);
|
|
487
|
+
const geometry = new THREE5.PlaneGeometry(1, 1);
|
|
488
|
+
let size = c.radius;
|
|
489
|
+
if (size <= 1) size *= radius;
|
|
490
|
+
size *= 2;
|
|
491
|
+
const texPath = `${basePath}/${c.image}`;
|
|
492
|
+
let blending = THREE5.NormalBlending;
|
|
493
|
+
if (c.blend === "additive") blending = THREE5.AdditiveBlending;
|
|
494
|
+
const material = createSmartMaterial({
|
|
495
|
+
uniforms: {
|
|
496
|
+
uMap: { value: this.textureLoader.load(texPath) },
|
|
497
|
+
// Placeholder, updated below
|
|
498
|
+
uOpacity: { value: c.opacity },
|
|
499
|
+
uSize: { value: size },
|
|
500
|
+
uImgRotation: { value: THREE5.MathUtils.degToRad(c.rotationDeg) },
|
|
501
|
+
uImgAspect: { value: c.aspectRatio ?? 1 }
|
|
502
|
+
// uScale, uAspect (screen) are injected by createSmartMaterial/globalUniforms
|
|
503
|
+
},
|
|
504
|
+
vertexShaderBody: `
|
|
505
|
+
uniform float uSize;
|
|
506
|
+
uniform float uImgRotation;
|
|
507
|
+
uniform float uImgAspect;
|
|
508
|
+
|
|
509
|
+
varying vec2 vUv;
|
|
510
|
+
|
|
511
|
+
void main() {
|
|
512
|
+
vUv = uv;
|
|
513
|
+
|
|
514
|
+
// 1. Project Center Point (Proven Method)
|
|
515
|
+
vec4 mvCenter = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
516
|
+
vec4 clipCenter = smartProject(mvCenter);
|
|
517
|
+
|
|
518
|
+
// 2. Project "Up" Point (World Zenith)
|
|
519
|
+
// Transform World Up (0,1,0) to View Space
|
|
520
|
+
vec3 viewUpDir = mat3(viewMatrix) * vec3(0.0, 1.0, 0.0);
|
|
521
|
+
// Offset center by a significant amount (1000.0) to ensure screen delta
|
|
522
|
+
vec4 mvUp = mvCenter + vec4(viewUpDir * 1000.0, 0.0);
|
|
523
|
+
vec4 clipUp = smartProject(mvUp);
|
|
524
|
+
|
|
525
|
+
// 3. Calculate Horizon Angle
|
|
526
|
+
vec2 screenCenter = clipCenter.xy / clipCenter.w;
|
|
527
|
+
vec2 screenUp = clipUp.xy / clipUp.w;
|
|
528
|
+
vec2 screenDelta = screenUp - screenCenter;
|
|
529
|
+
|
|
530
|
+
float horizonAngle = 0.0;
|
|
531
|
+
if (length(screenDelta) > 0.001) {
|
|
532
|
+
vec2 screenDir = normalize(screenDelta);
|
|
533
|
+
horizonAngle = atan(screenDir.y, screenDir.x) - 1.5708; // -90 deg
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// 4. Combine with User Rotation
|
|
537
|
+
float finalAngle = uImgRotation + horizonAngle;
|
|
538
|
+
|
|
539
|
+
// 5. Billboard Offset
|
|
540
|
+
vec2 offset = position.xy;
|
|
541
|
+
|
|
542
|
+
float cr = cos(finalAngle);
|
|
543
|
+
float sr = sin(finalAngle);
|
|
544
|
+
vec2 rotated = vec2(
|
|
545
|
+
offset.x * cr - offset.y * sr,
|
|
546
|
+
offset.x * sr + offset.y * cr
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
rotated.x *= uImgAspect;
|
|
550
|
+
|
|
551
|
+
float dist = length(mvCenter.xyz);
|
|
552
|
+
float scale = (uSize / dist) * uScale;
|
|
553
|
+
|
|
554
|
+
rotated *= scale;
|
|
555
|
+
rotated.x /= uAspect;
|
|
556
|
+
|
|
557
|
+
gl_Position = clipCenter;
|
|
558
|
+
gl_Position.xy += rotated * clipCenter.w;
|
|
559
|
+
|
|
560
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
561
|
+
}
|
|
562
|
+
`,
|
|
563
|
+
fragmentShader: `
|
|
564
|
+
uniform sampler2D uMap;
|
|
565
|
+
uniform float uOpacity;
|
|
566
|
+
varying vec2 vUv;
|
|
567
|
+
void main() {
|
|
568
|
+
float mask = getMaskAlpha();
|
|
569
|
+
if (mask < 0.01) discard;
|
|
570
|
+
vec4 tex = texture2D(uMap, vUv);
|
|
571
|
+
gl_FragColor = vec4(tex.rgb, tex.a * uOpacity * mask);
|
|
572
|
+
}
|
|
573
|
+
`,
|
|
574
|
+
transparent: true,
|
|
575
|
+
depthWrite: false,
|
|
576
|
+
depthTest: true,
|
|
577
|
+
blending,
|
|
578
|
+
side: THREE5.DoubleSide
|
|
579
|
+
});
|
|
580
|
+
material.uniforms.uMap.value = this.textureLoader.load(texPath, (tex) => {
|
|
581
|
+
if (c.aspectRatio === void 0 && tex.image.width && tex.image.height) {
|
|
582
|
+
const natAspect = tex.image.width / tex.image.height;
|
|
583
|
+
material.uniforms.uImgAspect.value = natAspect;
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
if (c.zBias) {
|
|
587
|
+
material.polygonOffset = true;
|
|
588
|
+
material.polygonOffsetFactor = -c.zBias;
|
|
589
|
+
}
|
|
590
|
+
const mesh = new THREE5.Mesh(geometry, material);
|
|
591
|
+
mesh.frustumCulled = false;
|
|
592
|
+
mesh.userData = { id: c.id, type: "constellation" };
|
|
593
|
+
mesh.position.copy(center);
|
|
594
|
+
this.root.add(mesh);
|
|
595
|
+
this.items.push({ config: c, mesh, material, baseOpacity: c.opacity });
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
update(fov, showArt) {
|
|
599
|
+
this.root.visible = showArt;
|
|
600
|
+
if (!showArt) return;
|
|
601
|
+
for (const item of this.items) {
|
|
602
|
+
const { fade } = item.config;
|
|
603
|
+
let opacity = fade.maxOpacity;
|
|
604
|
+
if (fov >= fade.zoomInStart) {
|
|
605
|
+
opacity = fade.maxOpacity;
|
|
606
|
+
} else if (fov <= fade.zoomInEnd) {
|
|
607
|
+
opacity = fade.minOpacity;
|
|
608
|
+
} else {
|
|
609
|
+
const t = (fade.zoomInStart - fov) / (fade.zoomInStart - fade.zoomInEnd);
|
|
610
|
+
opacity = THREE5.MathUtils.lerp(fade.maxOpacity, fade.minOpacity, t);
|
|
611
|
+
}
|
|
612
|
+
opacity = Math.min(Math.max(opacity, 0), 1);
|
|
613
|
+
item.material.uniforms.uOpacity.value = opacity;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
setHovered(id) {
|
|
617
|
+
this.hoveredId = id;
|
|
618
|
+
}
|
|
619
|
+
setFocused(id) {
|
|
620
|
+
this.focusedId = id;
|
|
621
|
+
}
|
|
622
|
+
dispose() {
|
|
623
|
+
this.clear();
|
|
624
|
+
this.root.removeFromParent();
|
|
625
|
+
}
|
|
626
|
+
clear() {
|
|
627
|
+
this.items.forEach((i) => {
|
|
628
|
+
this.root.remove(i.mesh);
|
|
629
|
+
i.material.dispose();
|
|
630
|
+
i.mesh.geometry.dispose();
|
|
631
|
+
});
|
|
632
|
+
this.items = [];
|
|
633
|
+
}
|
|
386
634
|
};
|
|
387
635
|
}
|
|
388
636
|
});
|
|
@@ -396,17 +644,37 @@ function createEngine({
|
|
|
396
644
|
container,
|
|
397
645
|
onSelect,
|
|
398
646
|
onHover,
|
|
399
|
-
onArrangementChange
|
|
647
|
+
onArrangementChange,
|
|
648
|
+
onFovChange
|
|
400
649
|
}) {
|
|
401
|
-
|
|
650
|
+
let hoveredBookId = null;
|
|
651
|
+
let focusedBookId = null;
|
|
652
|
+
let orderRevealEnabled = true;
|
|
653
|
+
let activeBookIndex = -1;
|
|
654
|
+
let orderRevealStrength = 0;
|
|
655
|
+
const hoverCooldowns = /* @__PURE__ */ new Map();
|
|
656
|
+
const COOLDOWN_MS = 2e3;
|
|
657
|
+
const bookIdToIndex = /* @__PURE__ */ new Map();
|
|
658
|
+
const renderer = new THREE5.WebGLRenderer({ antialias: true, alpha: false });
|
|
402
659
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
403
660
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
404
661
|
container.appendChild(renderer.domElement);
|
|
405
|
-
const scene = new
|
|
406
|
-
scene.background = new
|
|
407
|
-
const camera = new
|
|
662
|
+
const scene = new THREE5.Scene();
|
|
663
|
+
scene.background = new THREE5.Color(0);
|
|
664
|
+
const camera = new THREE5.PerspectiveCamera(60, 1, 0.1, 1e4);
|
|
408
665
|
camera.position.set(0, 0, 0);
|
|
409
666
|
camera.up.set(0, 1, 0);
|
|
667
|
+
function setHoveredBook(id) {
|
|
668
|
+
if (id === hoveredBookId) return;
|
|
669
|
+
const now = performance.now();
|
|
670
|
+
if (hoveredBookId) {
|
|
671
|
+
hoverCooldowns.set(hoveredBookId, now);
|
|
672
|
+
}
|
|
673
|
+
if (id) {
|
|
674
|
+
hoverCooldowns.get(id) || 0;
|
|
675
|
+
}
|
|
676
|
+
hoveredBookId = id;
|
|
677
|
+
}
|
|
410
678
|
let running = false;
|
|
411
679
|
let raf = 0;
|
|
412
680
|
const state = {
|
|
@@ -424,12 +692,15 @@ function createEngine({
|
|
|
424
692
|
draggedNodeId: null,
|
|
425
693
|
draggedStarIndex: -1,
|
|
426
694
|
draggedDist: 2e3,
|
|
427
|
-
draggedGroup: null
|
|
695
|
+
draggedGroup: null,
|
|
696
|
+
tempArrangement: {}
|
|
428
697
|
};
|
|
429
|
-
const mouseNDC = new
|
|
698
|
+
const mouseNDC = new THREE5.Vector2();
|
|
430
699
|
let isMouseInWindow = false;
|
|
431
|
-
let
|
|
700
|
+
let edgeHoverStart = 0;
|
|
701
|
+
let handlers = { onSelect, onHover, onArrangementChange, onFovChange };
|
|
432
702
|
let currentConfig;
|
|
703
|
+
const constellationLayer = new ConstellationArtworkLayer(scene);
|
|
433
704
|
function mix(a, b, t) {
|
|
434
705
|
return a * (1 - t) + b * t;
|
|
435
706
|
}
|
|
@@ -464,7 +735,7 @@ function createEngine({
|
|
|
464
735
|
const phi = Math.atan2(uvY, uvX);
|
|
465
736
|
const sinTheta = Math.sin(theta);
|
|
466
737
|
const cosTheta = Math.cos(theta);
|
|
467
|
-
return new
|
|
738
|
+
return new THREE5.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
|
|
468
739
|
}
|
|
469
740
|
function getMouseWorldVector(pixelX, pixelY, width, height) {
|
|
470
741
|
const aspect = width / height;
|
|
@@ -483,7 +754,7 @@ function createEngine({
|
|
|
483
754
|
const phi = Math.atan2(uvY, uvX);
|
|
484
755
|
const sinTheta = Math.sin(theta);
|
|
485
756
|
const cosTheta = Math.cos(theta);
|
|
486
|
-
const vView = new
|
|
757
|
+
const vView = new THREE5.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
|
|
487
758
|
return vView.applyQuaternion(camera.quaternion);
|
|
488
759
|
}
|
|
489
760
|
function smartProjectJS(worldPos) {
|
|
@@ -496,147 +767,187 @@ function createEngine({
|
|
|
496
767
|
const k = mix(kLinear, kStereo, blend);
|
|
497
768
|
return { x: k * dir.x, y: k * dir.y, z: dir.z };
|
|
498
769
|
}
|
|
499
|
-
const groundGroup = new
|
|
770
|
+
const groundGroup = new THREE5.Group();
|
|
500
771
|
scene.add(groundGroup);
|
|
501
772
|
function createGround() {
|
|
502
773
|
groundGroup.clear();
|
|
503
774
|
const radius = 995;
|
|
504
|
-
const geometry = new
|
|
775
|
+
const geometry = new THREE5.SphereGeometry(radius, 128, 64, 0, Math.PI * 2, Math.PI / 2 - 0.15, Math.PI / 2 + 0.15);
|
|
505
776
|
const material = createSmartMaterial({
|
|
506
|
-
uniforms: {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
777
|
+
uniforms: {
|
|
778
|
+
color: { value: new THREE5.Color(131587) },
|
|
779
|
+
// Very dark almost black
|
|
780
|
+
fogColor: { value: new THREE5.Color(331812) }
|
|
781
|
+
// Matches atmosphere bot color
|
|
782
|
+
},
|
|
783
|
+
vertexShaderBody: `
|
|
784
|
+
varying vec3 vPos;
|
|
785
|
+
varying vec3 vWorldPos;
|
|
786
|
+
void main() {
|
|
787
|
+
vPos = position;
|
|
788
|
+
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
789
|
+
gl_Position = smartProject(mvPosition);
|
|
790
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
791
|
+
vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
|
|
792
|
+
}
|
|
793
|
+
`,
|
|
794
|
+
fragmentShader: `
|
|
795
|
+
uniform vec3 color;
|
|
796
|
+
uniform vec3 fogColor;
|
|
797
|
+
varying vec3 vPos;
|
|
798
|
+
varying vec3 vWorldPos;
|
|
799
|
+
|
|
800
|
+
void main() {
|
|
801
|
+
float alphaMask = getMaskAlpha();
|
|
802
|
+
if (alphaMask < 0.01) discard;
|
|
803
|
+
|
|
804
|
+
// Procedural Horizon (Mountains)
|
|
805
|
+
float angle = atan(vPos.z, vPos.x);
|
|
806
|
+
|
|
807
|
+
// Simple FBM-like terrain
|
|
808
|
+
float h = 0.0;
|
|
809
|
+
h += sin(angle * 6.0) * 20.0;
|
|
810
|
+
h += sin(angle * 13.0 + 1.0) * 10.0;
|
|
811
|
+
h += sin(angle * 29.0 + 2.0) * 5.0;
|
|
812
|
+
h += sin(angle * 63.0 + 4.0) * 2.0;
|
|
813
|
+
|
|
814
|
+
// Base horizon offset (lift slightly)
|
|
815
|
+
float terrainHeight = h + 10.0;
|
|
816
|
+
|
|
817
|
+
if (vPos.y > terrainHeight) discard;
|
|
818
|
+
|
|
819
|
+
// Atmospheric Haze / Fog on the ground
|
|
820
|
+
// Mix ground color with fog color based on vertical height (fade into horizon)
|
|
821
|
+
// Closer to horizon (higher y) -> more fog
|
|
822
|
+
float fogFactor = smoothstep(-100.0, terrainHeight, vPos.y);
|
|
823
|
+
vec3 finalCol = mix(color, fogColor, fogFactor * 0.5);
|
|
824
|
+
|
|
825
|
+
gl_FragColor = vec4(finalCol, 1.0);
|
|
826
|
+
}
|
|
827
|
+
`,
|
|
828
|
+
side: THREE5.BackSide,
|
|
510
829
|
transparent: false,
|
|
511
830
|
depthWrite: true,
|
|
512
831
|
depthTest: true
|
|
513
832
|
});
|
|
514
|
-
const ground = new
|
|
833
|
+
const ground = new THREE5.Mesh(geometry, material);
|
|
515
834
|
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);
|
|
524
|
-
}
|
|
525
835
|
}
|
|
836
|
+
let atmosphereMesh = null;
|
|
526
837
|
function createAtmosphere() {
|
|
527
|
-
const geometry = new
|
|
838
|
+
const geometry = new THREE5.SphereGeometry(990, 64, 64);
|
|
528
839
|
const material = createSmartMaterial({
|
|
529
|
-
uniforms: { top: { value: new THREE4.Color(0) }, bot: { value: new THREE4.Color(1712172) } },
|
|
530
840
|
vertexShaderBody: `
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
`,
|
|
841
|
+
varying vec3 vWorldNormal;
|
|
842
|
+
void main() {
|
|
843
|
+
vWorldNormal = normalize(position);
|
|
844
|
+
vec4 mv = modelViewMatrix * vec4(position, 1.0);
|
|
845
|
+
gl_Position = smartProject(mv);
|
|
846
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
847
|
+
}`,
|
|
547
848
|
fragmentShader: `
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
849
|
+
varying vec3 vWorldNormal;
|
|
850
|
+
|
|
851
|
+
uniform float uAtmGlow;
|
|
852
|
+
uniform float uAtmDark;
|
|
853
|
+
uniform vec3 uColorHorizon;
|
|
854
|
+
uniform vec3 uColorZenith;
|
|
855
|
+
|
|
856
|
+
void main() {
|
|
857
|
+
float alphaMask = getMaskAlpha();
|
|
858
|
+
if (alphaMask < 0.01) discard;
|
|
859
|
+
|
|
860
|
+
// Altitude angle (Y is up)
|
|
861
|
+
float h = normalize(vWorldNormal).y;
|
|
862
|
+
|
|
863
|
+
// Gradient Logic
|
|
864
|
+
// 1. Base gradient from Horizon to Zenith
|
|
865
|
+
float t = smoothstep(-0.1, 0.5, h);
|
|
866
|
+
|
|
867
|
+
// Non-linear mix for realistic sky falloff
|
|
868
|
+
// Zenith darkness adjustment
|
|
869
|
+
vec3 skyColor = mix(uColorHorizon * uAtmGlow, uColorZenith * (1.0 - uAtmDark), pow(t, 0.6));
|
|
870
|
+
|
|
871
|
+
// 2. Horizon Glow Band (Simulate scattering/haze layer)
|
|
872
|
+
float horizonBand = exp(-15.0 * abs(h - 0.02)); // Sharp peak near 0
|
|
873
|
+
skyColor += uColorHorizon * horizonBand * 0.5 * uAtmGlow;
|
|
874
|
+
|
|
875
|
+
gl_FragColor = vec4(skyColor, 1.0);
|
|
876
|
+
}
|
|
877
|
+
`,
|
|
878
|
+
side: THREE5.BackSide,
|
|
571
879
|
depthWrite: false,
|
|
572
880
|
depthTest: true
|
|
573
881
|
});
|
|
574
|
-
const atm = new
|
|
882
|
+
const atm = new THREE5.Mesh(geometry, material);
|
|
883
|
+
atmosphereMesh = atm;
|
|
575
884
|
groundGroup.add(atm);
|
|
576
885
|
}
|
|
577
|
-
const backdropGroup = new
|
|
886
|
+
const backdropGroup = new THREE5.Group();
|
|
578
887
|
scene.add(backdropGroup);
|
|
579
|
-
function createBackdropStars() {
|
|
888
|
+
function createBackdropStars(count = 31e3) {
|
|
580
889
|
backdropGroup.clear();
|
|
581
|
-
|
|
890
|
+
while (backdropGroup.children.length > 0) {
|
|
891
|
+
const c = backdropGroup.children[0];
|
|
892
|
+
backdropGroup.remove(c);
|
|
893
|
+
if (c.geometry) c.geometry.dispose();
|
|
894
|
+
if (c.material) c.material.dispose();
|
|
895
|
+
}
|
|
896
|
+
const geometry = new THREE5.BufferGeometry();
|
|
582
897
|
const positions = [];
|
|
583
898
|
const sizes = [];
|
|
584
899
|
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
900
|
const r = 2500;
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
-
}
|
|
901
|
+
for (let i = 0; i < count; i++) {
|
|
902
|
+
const u = Math.random();
|
|
903
|
+
const v = Math.random();
|
|
904
|
+
const theta = 2 * Math.PI * u;
|
|
905
|
+
const phi = Math.acos(2 * v - 1);
|
|
906
|
+
const x = r * Math.sin(phi) * Math.cos(theta);
|
|
907
|
+
const y = r * Math.cos(phi);
|
|
908
|
+
const z = r * Math.sin(phi) * Math.sin(theta);
|
|
617
909
|
positions.push(x, y, z);
|
|
618
|
-
const size =
|
|
910
|
+
const size = 1 + -Math.log(Math.random()) * 0.8 * 1.5;
|
|
619
911
|
sizes.push(size);
|
|
620
|
-
|
|
621
|
-
const c = colorPalette[cIndex];
|
|
622
|
-
colors.push(c.r, c.g, c.b);
|
|
912
|
+
colors.push(1, 1, 1);
|
|
623
913
|
}
|
|
624
|
-
geometry.setAttribute("position", new
|
|
625
|
-
geometry.setAttribute("size", new
|
|
626
|
-
geometry.setAttribute("color", new
|
|
914
|
+
geometry.setAttribute("position", new THREE5.Float32BufferAttribute(positions, 3));
|
|
915
|
+
geometry.setAttribute("size", new THREE5.Float32BufferAttribute(sizes, 1));
|
|
916
|
+
geometry.setAttribute("color", new THREE5.Float32BufferAttribute(colors, 3));
|
|
627
917
|
const material = createSmartMaterial({
|
|
628
|
-
uniforms: {
|
|
918
|
+
uniforms: {
|
|
919
|
+
pixelRatio: { value: renderer.getPixelRatio() },
|
|
920
|
+
uScale: globalUniforms.uScale
|
|
921
|
+
},
|
|
629
922
|
vertexShaderBody: `
|
|
630
923
|
attribute float size;
|
|
631
924
|
attribute vec3 color;
|
|
632
925
|
varying vec3 vColor;
|
|
633
926
|
uniform float pixelRatio;
|
|
927
|
+
|
|
928
|
+
uniform float uAtmExtinction;
|
|
929
|
+
|
|
634
930
|
void main() {
|
|
635
|
-
|
|
931
|
+
vec3 nPos = normalize(position);
|
|
932
|
+
float altitude = nPos.y;
|
|
933
|
+
|
|
934
|
+
// Simple Extinction & Horizon Fade
|
|
935
|
+
float horizonFade = smoothstep(-0.1, 0.1, altitude);
|
|
936
|
+
float airmass = 1.0 / (max(0.05, altitude + 0.05));
|
|
937
|
+
float extinction = exp(-uAtmExtinction * 0.15 * airmass);
|
|
938
|
+
|
|
939
|
+
// Boost intensity significantly (3.0x)
|
|
940
|
+
vColor = color * 3.0 * extinction * horizonFade;
|
|
941
|
+
|
|
636
942
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
637
943
|
gl_Position = smartProject(mvPosition);
|
|
638
944
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
639
|
-
|
|
945
|
+
|
|
946
|
+
// Non-linear scale with zoom to keep stars looking like points
|
|
947
|
+
// pow(uScale, 0.5) prevents them from getting too large at low FOV
|
|
948
|
+
float zoomScale = pow(uScale, 0.5);
|
|
949
|
+
|
|
950
|
+
gl_PointSize = size * zoomScale * 0.5 * pixelRatio * (800.0 / -mvPosition.z) * horizonFade;
|
|
640
951
|
}
|
|
641
952
|
`,
|
|
642
953
|
fragmentShader: `
|
|
@@ -647,31 +958,81 @@ function createEngine({
|
|
|
647
958
|
if (dist > 1.0) discard;
|
|
648
959
|
float alphaMask = getMaskAlpha();
|
|
649
960
|
if (alphaMask < 0.01) discard;
|
|
650
|
-
|
|
651
|
-
|
|
961
|
+
|
|
962
|
+
// Sharp falloff for intense point look
|
|
963
|
+
float alpha = exp(-4.0 * dist * dist);
|
|
652
964
|
gl_FragColor = vec4(vColor, alpha * alphaMask);
|
|
653
965
|
}
|
|
654
966
|
`,
|
|
655
967
|
transparent: true,
|
|
656
968
|
depthWrite: false,
|
|
657
|
-
depthTest: true
|
|
969
|
+
depthTest: true,
|
|
970
|
+
blending: THREE5.AdditiveBlending
|
|
658
971
|
});
|
|
659
|
-
const points = new
|
|
972
|
+
const points = new THREE5.Points(geometry, material);
|
|
660
973
|
points.frustumCulled = false;
|
|
661
974
|
backdropGroup.add(points);
|
|
662
975
|
}
|
|
663
976
|
createGround();
|
|
664
977
|
createAtmosphere();
|
|
665
978
|
createBackdropStars();
|
|
666
|
-
const raycaster = new
|
|
979
|
+
const raycaster = new THREE5.Raycaster();
|
|
667
980
|
raycaster.params.Points.threshold = 5;
|
|
668
|
-
new
|
|
669
|
-
const root = new
|
|
981
|
+
new THREE5.Vector2();
|
|
982
|
+
const root = new THREE5.Group();
|
|
670
983
|
scene.add(root);
|
|
671
984
|
const nodeById = /* @__PURE__ */ new Map();
|
|
672
985
|
const starIndexToId = [];
|
|
673
986
|
const dynamicLabels = [];
|
|
987
|
+
const hoverLabelMat = createSmartMaterial({
|
|
988
|
+
uniforms: {
|
|
989
|
+
uMap: { value: null },
|
|
990
|
+
uSize: { value: new THREE5.Vector2(1, 1) },
|
|
991
|
+
uAlpha: { value: 0 },
|
|
992
|
+
uAngle: { value: 0 }
|
|
993
|
+
},
|
|
994
|
+
vertexShaderBody: `
|
|
995
|
+
uniform vec2 uSize;
|
|
996
|
+
uniform float uAngle;
|
|
997
|
+
varying vec2 vUv;
|
|
998
|
+
void main() {
|
|
999
|
+
vUv = uv;
|
|
1000
|
+
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
1001
|
+
vec4 projected = smartProject(mvPos);
|
|
1002
|
+
|
|
1003
|
+
float c = cos(uAngle);
|
|
1004
|
+
float s = sin(uAngle);
|
|
1005
|
+
mat2 rot = mat2(c, -s, s, c);
|
|
1006
|
+
vec2 offset = rot * (position.xy * uSize);
|
|
1007
|
+
|
|
1008
|
+
projected.xy += offset / vec2(uAspect, 1.0);
|
|
1009
|
+
gl_Position = projected;
|
|
1010
|
+
}
|
|
1011
|
+
`,
|
|
1012
|
+
fragmentShader: `
|
|
1013
|
+
uniform sampler2D uMap;
|
|
1014
|
+
uniform float uAlpha;
|
|
1015
|
+
varying vec2 vUv;
|
|
1016
|
+
void main() {
|
|
1017
|
+
float mask = getMaskAlpha();
|
|
1018
|
+
if (mask < 0.01) discard;
|
|
1019
|
+
vec4 tex = texture2D(uMap, vUv);
|
|
1020
|
+
gl_FragColor = vec4(tex.rgb, tex.a * uAlpha * mask);
|
|
1021
|
+
}
|
|
1022
|
+
`,
|
|
1023
|
+
transparent: true,
|
|
1024
|
+
depthWrite: false,
|
|
1025
|
+
depthTest: false
|
|
1026
|
+
// Always on top of stars
|
|
1027
|
+
});
|
|
1028
|
+
const hoverLabelMesh = new THREE5.Mesh(new THREE5.PlaneGeometry(1, 1), hoverLabelMat);
|
|
1029
|
+
hoverLabelMesh.visible = false;
|
|
1030
|
+
hoverLabelMesh.renderOrder = 999;
|
|
1031
|
+
hoverLabelMesh.frustumCulled = false;
|
|
1032
|
+
root.add(hoverLabelMesh);
|
|
1033
|
+
let currentHoverNodeId = null;
|
|
674
1034
|
let constellationLines = null;
|
|
1035
|
+
let boundaryLines = null;
|
|
675
1036
|
let starPoints = null;
|
|
676
1037
|
function clearRoot() {
|
|
677
1038
|
for (const child of [...root.children]) {
|
|
@@ -687,6 +1048,7 @@ function createEngine({
|
|
|
687
1048
|
starIndexToId.length = 0;
|
|
688
1049
|
dynamicLabels.length = 0;
|
|
689
1050
|
constellationLines = null;
|
|
1051
|
+
boundaryLines = null;
|
|
690
1052
|
starPoints = null;
|
|
691
1053
|
}
|
|
692
1054
|
function createTextTexture(text, color = "#ffffff") {
|
|
@@ -694,58 +1056,102 @@ function createEngine({
|
|
|
694
1056
|
const ctx = canvas.getContext("2d");
|
|
695
1057
|
if (!ctx) return null;
|
|
696
1058
|
const fontSize = 96;
|
|
697
|
-
|
|
1059
|
+
const font = `400 ${fontSize}px "Inter", system-ui, sans-serif`;
|
|
1060
|
+
ctx.font = font;
|
|
698
1061
|
const metrics = ctx.measureText(text);
|
|
699
1062
|
const w = Math.ceil(metrics.width);
|
|
700
1063
|
const h = Math.ceil(fontSize * 1.2);
|
|
701
1064
|
canvas.width = w;
|
|
702
1065
|
canvas.height = h;
|
|
703
|
-
ctx.font =
|
|
1066
|
+
ctx.font = font;
|
|
704
1067
|
ctx.fillStyle = color;
|
|
705
1068
|
ctx.textAlign = "center";
|
|
706
1069
|
ctx.textBaseline = "middle";
|
|
707
1070
|
ctx.fillText(text, w / 2, h / 2);
|
|
708
|
-
const tex = new
|
|
709
|
-
tex.minFilter =
|
|
1071
|
+
const tex = new THREE5.CanvasTexture(canvas);
|
|
1072
|
+
tex.minFilter = THREE5.LinearFilter;
|
|
710
1073
|
return { tex, aspect: w / h };
|
|
711
1074
|
}
|
|
712
1075
|
function getPosition(n) {
|
|
713
1076
|
if (currentConfig?.arrangement) {
|
|
714
1077
|
const arr = currentConfig.arrangement[n.id];
|
|
715
|
-
if (arr)
|
|
1078
|
+
if (arr) {
|
|
1079
|
+
if (arr.position[2] === 0) {
|
|
1080
|
+
const x = arr.position[0];
|
|
1081
|
+
const y = arr.position[1];
|
|
1082
|
+
const radius = currentConfig.layout?.radius ?? 2e3;
|
|
1083
|
+
const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
|
|
1084
|
+
const phi = Math.atan2(y, x);
|
|
1085
|
+
const theta = r_norm * (Math.PI / 2);
|
|
1086
|
+
return new THREE5.Vector3(
|
|
1087
|
+
Math.sin(theta) * Math.cos(phi),
|
|
1088
|
+
Math.cos(theta),
|
|
1089
|
+
Math.sin(theta) * Math.sin(phi)
|
|
1090
|
+
).multiplyScalar(radius);
|
|
1091
|
+
}
|
|
1092
|
+
return new THREE5.Vector3(arr.position[0], arr.position[1], arr.position[2]);
|
|
1093
|
+
}
|
|
716
1094
|
}
|
|
717
|
-
return new
|
|
1095
|
+
return new THREE5.Vector3(n.meta?.x ?? 0, n.meta?.y ?? 0, n.meta?.z ?? 0);
|
|
718
1096
|
}
|
|
719
1097
|
function getBoundaryPoint(angle, t, radius) {
|
|
720
1098
|
const y = 0.05 + t * (1 - 0.05);
|
|
721
1099
|
const rY = Math.sqrt(1 - y * y);
|
|
722
1100
|
const x = Math.cos(angle) * rY;
|
|
723
1101
|
const z = Math.sin(angle) * rY;
|
|
724
|
-
return new
|
|
1102
|
+
return new THREE5.Vector3(x, y, z).multiplyScalar(radius);
|
|
725
1103
|
}
|
|
726
1104
|
function buildFromModel(model, cfg) {
|
|
727
1105
|
clearRoot();
|
|
728
|
-
|
|
1106
|
+
bookIdToIndex.clear();
|
|
1107
|
+
scene.background = cfg.background && cfg.background !== "transparent" ? new THREE5.Color(cfg.background) : new THREE5.Color(0);
|
|
729
1108
|
const layoutCfg = { ...cfg.layout, radius: cfg.layout?.radius ?? 2e3 };
|
|
730
1109
|
const laidOut = computeLayoutPositions(model, layoutCfg);
|
|
1110
|
+
const divisionPositions = /* @__PURE__ */ new Map();
|
|
1111
|
+
if (cfg.arrangement) {
|
|
1112
|
+
const divMap = /* @__PURE__ */ new Map();
|
|
1113
|
+
for (const n of laidOut.nodes) {
|
|
1114
|
+
if (n.level === 2 && n.parent) {
|
|
1115
|
+
const list = divMap.get(n.parent) ?? [];
|
|
1116
|
+
list.push(n);
|
|
1117
|
+
divMap.set(n.parent, list);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
for (const [divId, books] of divMap.entries()) {
|
|
1121
|
+
const centroid = new THREE5.Vector3();
|
|
1122
|
+
let count = 0;
|
|
1123
|
+
for (const b of books) {
|
|
1124
|
+
const p = getPosition(b);
|
|
1125
|
+
centroid.add(p);
|
|
1126
|
+
count++;
|
|
1127
|
+
}
|
|
1128
|
+
if (count > 0) {
|
|
1129
|
+
centroid.divideScalar(count);
|
|
1130
|
+
divisionPositions.set(divId, centroid);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
731
1134
|
const starPositions = [];
|
|
732
1135
|
const starSizes = [];
|
|
733
1136
|
const starColors = [];
|
|
1137
|
+
const starPhases = [];
|
|
1138
|
+
const starBookIndices = [];
|
|
1139
|
+
const starChapterIndices = [];
|
|
734
1140
|
const SPECTRAL_COLORS = [
|
|
735
|
-
new
|
|
736
|
-
// O -
|
|
737
|
-
new
|
|
738
|
-
// B -
|
|
739
|
-
new
|
|
740
|
-
// A - White
|
|
741
|
-
new
|
|
1141
|
+
new THREE5.Color(14544639),
|
|
1142
|
+
// O - Blueish White
|
|
1143
|
+
new THREE5.Color(15660287),
|
|
1144
|
+
// B - White
|
|
1145
|
+
new THREE5.Color(16317695),
|
|
1146
|
+
// A - White
|
|
1147
|
+
new THREE5.Color(16777208),
|
|
742
1148
|
// F - White
|
|
743
|
-
new
|
|
744
|
-
// G -
|
|
745
|
-
new
|
|
746
|
-
// K -
|
|
747
|
-
new
|
|
748
|
-
// M - Orange
|
|
1149
|
+
new THREE5.Color(16775406),
|
|
1150
|
+
// G - Yellowish White
|
|
1151
|
+
new THREE5.Color(16773085),
|
|
1152
|
+
// K - Pale Orange
|
|
1153
|
+
new THREE5.Color(16771788)
|
|
1154
|
+
// M - Light Orange
|
|
749
1155
|
];
|
|
750
1156
|
let minWeight = Infinity;
|
|
751
1157
|
let maxWeight = -Infinity;
|
|
@@ -770,32 +1176,62 @@ function createEngine({
|
|
|
770
1176
|
let baseSize = 3.5;
|
|
771
1177
|
if (typeof n.weight === "number") {
|
|
772
1178
|
const t = (n.weight - minWeight) / (maxWeight - minWeight);
|
|
773
|
-
baseSize =
|
|
1179
|
+
baseSize = 0.1 + Math.pow(t, 0.5) * 11.9;
|
|
774
1180
|
}
|
|
775
1181
|
starSizes.push(baseSize);
|
|
776
1182
|
const colorIdx = Math.floor(Math.pow(Math.random(), 1.5) * SPECTRAL_COLORS.length);
|
|
777
1183
|
const c = SPECTRAL_COLORS[Math.min(colorIdx, SPECTRAL_COLORS.length - 1)];
|
|
778
1184
|
starColors.push(c.r, c.g, c.b);
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1185
|
+
starPhases.push(Math.random() * Math.PI * 2);
|
|
1186
|
+
let bIdx = -1;
|
|
1187
|
+
if (n.parent) {
|
|
1188
|
+
if (!bookIdToIndex.has(n.parent)) {
|
|
1189
|
+
bookIdToIndex.set(n.parent, bookIdToIndex.size + 1);
|
|
1190
|
+
}
|
|
1191
|
+
bIdx = bookIdToIndex.get(n.parent);
|
|
1192
|
+
}
|
|
1193
|
+
starBookIndices.push(bIdx);
|
|
1194
|
+
let cIdx = 0;
|
|
1195
|
+
if (n.meta?.chapter) cIdx = Number(n.meta.chapter);
|
|
1196
|
+
starChapterIndices.push(cIdx);
|
|
1197
|
+
}
|
|
1198
|
+
if (n.level === 1 || n.level === 2 || n.level === 3) {
|
|
1199
|
+
let color = "#ffffff";
|
|
1200
|
+
if (n.level === 1) color = "#38bdf8";
|
|
1201
|
+
else if (n.level === 2) color = "#cbd5e1";
|
|
1202
|
+
else if (n.level === 3) color = "#94a3b8";
|
|
1203
|
+
let labelText = n.label;
|
|
1204
|
+
if (n.level === 3 && n.meta?.chapter) {
|
|
1205
|
+
labelText = String(n.meta.chapter);
|
|
1206
|
+
}
|
|
1207
|
+
const texRes = createTextTexture(labelText, color);
|
|
782
1208
|
if (texRes) {
|
|
783
|
-
|
|
784
|
-
|
|
1209
|
+
let baseScale = 0.05;
|
|
1210
|
+
if (n.level === 1) baseScale = 0.08;
|
|
1211
|
+
else if (n.level === 2) baseScale = 0.04;
|
|
1212
|
+
else if (n.level === 3) baseScale = 0.03;
|
|
1213
|
+
const size = new THREE5.Vector2(baseScale * texRes.aspect, baseScale);
|
|
785
1214
|
const mat = createSmartMaterial({
|
|
786
1215
|
uniforms: {
|
|
787
1216
|
uMap: { value: texRes.tex },
|
|
788
1217
|
uSize: { value: size },
|
|
789
|
-
uAlpha: { value: 0 }
|
|
1218
|
+
uAlpha: { value: 0 },
|
|
1219
|
+
uAngle: { value: 0 }
|
|
790
1220
|
},
|
|
791
1221
|
vertexShaderBody: `
|
|
792
1222
|
uniform vec2 uSize;
|
|
1223
|
+
uniform float uAngle;
|
|
793
1224
|
varying vec2 vUv;
|
|
794
1225
|
void main() {
|
|
795
1226
|
vUv = uv;
|
|
796
1227
|
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
797
1228
|
vec4 projected = smartProject(mvPos);
|
|
798
|
-
|
|
1229
|
+
|
|
1230
|
+
float c = cos(uAngle);
|
|
1231
|
+
float s = sin(uAngle);
|
|
1232
|
+
mat2 rot = mat2(c, -s, s, c);
|
|
1233
|
+
vec2 offset = rot * (position.xy * uSize);
|
|
1234
|
+
|
|
799
1235
|
projected.xy += offset / vec2(uAspect, 1.0);
|
|
800
1236
|
gl_Position = projected;
|
|
801
1237
|
}
|
|
@@ -815,8 +1251,19 @@ function createEngine({
|
|
|
815
1251
|
depthWrite: false,
|
|
816
1252
|
depthTest: true
|
|
817
1253
|
});
|
|
818
|
-
const mesh = new
|
|
819
|
-
|
|
1254
|
+
const mesh = new THREE5.Mesh(new THREE5.PlaneGeometry(1, 1), mat);
|
|
1255
|
+
let p = getPosition(n);
|
|
1256
|
+
if (n.level === 1) {
|
|
1257
|
+
if (divisionPositions.has(n.id)) {
|
|
1258
|
+
p.copy(divisionPositions.get(n.id));
|
|
1259
|
+
}
|
|
1260
|
+
const r = layoutCfg.radius * 0.95;
|
|
1261
|
+
const angle = Math.atan2(p.z, p.x);
|
|
1262
|
+
p.set(r * Math.cos(angle), 150, r * Math.sin(angle));
|
|
1263
|
+
} else if (n.level === 3) {
|
|
1264
|
+
p.y += 30;
|
|
1265
|
+
p.multiplyScalar(1.001);
|
|
1266
|
+
}
|
|
820
1267
|
mesh.position.set(p.x, p.y, p.z);
|
|
821
1268
|
mesh.scale.set(size.x, size.y, 1);
|
|
822
1269
|
mesh.frustumCulled = false;
|
|
@@ -826,47 +1273,119 @@ function createEngine({
|
|
|
826
1273
|
}
|
|
827
1274
|
}
|
|
828
1275
|
}
|
|
829
|
-
const starGeo = new
|
|
830
|
-
starGeo.setAttribute("position", new
|
|
831
|
-
starGeo.setAttribute("size", new
|
|
832
|
-
starGeo.setAttribute("color", new
|
|
1276
|
+
const starGeo = new THREE5.BufferGeometry();
|
|
1277
|
+
starGeo.setAttribute("position", new THREE5.Float32BufferAttribute(starPositions, 3));
|
|
1278
|
+
starGeo.setAttribute("size", new THREE5.Float32BufferAttribute(starSizes, 1));
|
|
1279
|
+
starGeo.setAttribute("color", new THREE5.Float32BufferAttribute(starColors, 3));
|
|
1280
|
+
starGeo.setAttribute("phase", new THREE5.Float32BufferAttribute(starPhases, 1));
|
|
1281
|
+
starGeo.setAttribute("bookIndex", new THREE5.Float32BufferAttribute(starBookIndices, 1));
|
|
1282
|
+
starGeo.setAttribute("chapterIndex", new THREE5.Float32BufferAttribute(starChapterIndices, 1));
|
|
833
1283
|
const starMat = createSmartMaterial({
|
|
834
|
-
uniforms: {
|
|
1284
|
+
uniforms: {
|
|
1285
|
+
pixelRatio: { value: renderer.getPixelRatio() },
|
|
1286
|
+
uScale: globalUniforms.uScale,
|
|
1287
|
+
uTime: globalUniforms.uTime,
|
|
1288
|
+
uActiveBookIndex: { value: -1 },
|
|
1289
|
+
uOrderRevealStrength: { value: 0 },
|
|
1290
|
+
uGlobalDimFactor: { value: ORDER_REVEAL_CONFIG.globalDim },
|
|
1291
|
+
uPulseParams: { value: new THREE5.Vector3(
|
|
1292
|
+
ORDER_REVEAL_CONFIG.pulseDuration,
|
|
1293
|
+
ORDER_REVEAL_CONFIG.delayPerChapter,
|
|
1294
|
+
ORDER_REVEAL_CONFIG.pulseAmplitude
|
|
1295
|
+
) }
|
|
1296
|
+
},
|
|
835
1297
|
vertexShaderBody: `
|
|
836
1298
|
attribute float size;
|
|
837
1299
|
attribute vec3 color;
|
|
1300
|
+
attribute float phase;
|
|
1301
|
+
attribute float bookIndex;
|
|
1302
|
+
attribute float chapterIndex;
|
|
1303
|
+
|
|
838
1304
|
varying vec3 vColor;
|
|
839
1305
|
uniform float pixelRatio;
|
|
1306
|
+
|
|
1307
|
+
uniform float uTime;
|
|
1308
|
+
uniform float uAtmExtinction;
|
|
1309
|
+
uniform float uAtmTwinkle;
|
|
1310
|
+
|
|
1311
|
+
uniform float uActiveBookIndex;
|
|
1312
|
+
uniform float uOrderRevealStrength;
|
|
1313
|
+
uniform float uGlobalDimFactor;
|
|
1314
|
+
uniform vec3 uPulseParams;
|
|
1315
|
+
|
|
840
1316
|
void main() {
|
|
841
|
-
|
|
1317
|
+
vec3 nPos = normalize(position);
|
|
1318
|
+
|
|
1319
|
+
// 1. Altitude (Y is UP)
|
|
1320
|
+
float altitude = nPos.y;
|
|
1321
|
+
|
|
1322
|
+
// 2. Atmospheric Extinction (Airmass approximation)
|
|
1323
|
+
float airmass = 1.0 / (max(0.02, altitude + 0.05));
|
|
1324
|
+
float extinction = exp(-uAtmExtinction * 0.1 * airmass);
|
|
1325
|
+
|
|
1326
|
+
// Fade out stars below horizon
|
|
1327
|
+
float horizonFade = smoothstep(-0.1, 0.05, altitude);
|
|
1328
|
+
|
|
1329
|
+
// 3. Scintillation
|
|
1330
|
+
float turbulence = 1.0 + (1.0 - smoothstep(0.0, 1.0, altitude)) * 2.0;
|
|
1331
|
+
float twinkle = sin(uTime * 3.0 + phase + position.x * 0.01) * 0.5 + 0.5;
|
|
1332
|
+
float scintillation = mix(1.0, twinkle * 2.0, uAtmTwinkle * 0.5 * turbulence);
|
|
1333
|
+
|
|
1334
|
+
// --- Order Reveal Logic ---
|
|
1335
|
+
float isTarget = 1.0 - min(1.0, abs(bookIndex - uActiveBookIndex));
|
|
1336
|
+
|
|
1337
|
+
// Dimming
|
|
1338
|
+
float dimFactor = mix(1.0, uGlobalDimFactor, uOrderRevealStrength * (1.0 - isTarget));
|
|
1339
|
+
|
|
1340
|
+
// Pulse
|
|
1341
|
+
float delay = chapterIndex * uPulseParams.y;
|
|
1342
|
+
float cycleDuration = uPulseParams.x * 2.5;
|
|
1343
|
+
float t = mod(uTime - delay, cycleDuration);
|
|
1344
|
+
|
|
1345
|
+
float pulse = smoothstep(0.0, 0.2, t) * (1.0 - smoothstep(0.4, uPulseParams.x, t));
|
|
1346
|
+
pulse = max(0.0, pulse);
|
|
1347
|
+
|
|
1348
|
+
float activePulse = pulse * uPulseParams.z * isTarget * uOrderRevealStrength;
|
|
1349
|
+
|
|
1350
|
+
vec3 baseColor = color * extinction * horizonFade * scintillation;
|
|
1351
|
+
vColor = baseColor * dimFactor;
|
|
1352
|
+
vColor += vec3(1.0, 0.8, 0.4) * activePulse;
|
|
1353
|
+
|
|
842
1354
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
843
1355
|
gl_Position = smartProject(mvPosition);
|
|
844
1356
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
845
|
-
|
|
1357
|
+
|
|
1358
|
+
float sizeBoost = 1.0 + activePulse * 0.8;
|
|
1359
|
+
gl_PointSize = (size * sizeBoost * 1.5) * uScale * pixelRatio * (2000.0 / -mvPosition.z) * horizonFade;
|
|
846
1360
|
}
|
|
847
1361
|
`,
|
|
848
1362
|
fragmentShader: `
|
|
849
1363
|
varying vec3 vColor;
|
|
850
1364
|
void main() {
|
|
851
1365
|
vec2 coord = gl_PointCoord - vec2(0.5);
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
if (dist > 1.0) discard;
|
|
1366
|
+
float d = length(coord) * 2.0;
|
|
1367
|
+
if (d > 1.0) discard;
|
|
855
1368
|
|
|
856
1369
|
float alphaMask = getMaskAlpha();
|
|
857
1370
|
if (alphaMask < 0.01) discard;
|
|
858
1371
|
|
|
859
|
-
|
|
860
|
-
|
|
1372
|
+
float dd = d * d;
|
|
1373
|
+
// Stellarium Profile
|
|
1374
|
+
float core = exp(-20.0 * dd);
|
|
1375
|
+
float halo = exp(-4.0 * dd);
|
|
861
1376
|
|
|
862
|
-
|
|
1377
|
+
vec3 cCore = vec3(1.0) * core * 1.5;
|
|
1378
|
+
vec3 cHalo = vColor * halo * 0.6;
|
|
1379
|
+
|
|
1380
|
+
gl_FragColor = vec4((cCore + cHalo) * alphaMask, 1.0);
|
|
863
1381
|
}
|
|
864
1382
|
`,
|
|
865
1383
|
transparent: true,
|
|
866
1384
|
depthWrite: false,
|
|
867
|
-
depthTest: true
|
|
1385
|
+
depthTest: true,
|
|
1386
|
+
blending: THREE5.AdditiveBlending
|
|
868
1387
|
});
|
|
869
|
-
starPoints = new
|
|
1388
|
+
starPoints = new THREE5.Points(starGeo, starMat);
|
|
870
1389
|
starPoints.frustumCulled = false;
|
|
871
1390
|
root.add(starPoints);
|
|
872
1391
|
const linePoints = [];
|
|
@@ -892,31 +1411,119 @@ function createEngine({
|
|
|
892
1411
|
}
|
|
893
1412
|
}
|
|
894
1413
|
if (linePoints.length > 0) {
|
|
895
|
-
const lineGeo = new
|
|
896
|
-
lineGeo.setAttribute("position", new
|
|
1414
|
+
const lineGeo = new THREE5.BufferGeometry();
|
|
1415
|
+
lineGeo.setAttribute("position", new THREE5.Float32BufferAttribute(linePoints, 3));
|
|
897
1416
|
const lineMat = createSmartMaterial({
|
|
898
|
-
uniforms: { color: { value: new
|
|
1417
|
+
uniforms: { color: { value: new THREE5.Color(11193599) } },
|
|
899
1418
|
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.
|
|
1419
|
+
fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.4 * alphaMask); }`,
|
|
901
1420
|
transparent: true,
|
|
902
1421
|
depthWrite: false,
|
|
903
|
-
blending:
|
|
1422
|
+
blending: THREE5.AdditiveBlending
|
|
904
1423
|
});
|
|
905
|
-
constellationLines = new
|
|
1424
|
+
constellationLines = new THREE5.LineSegments(lineGeo, lineMat);
|
|
906
1425
|
constellationLines.frustumCulled = false;
|
|
907
1426
|
root.add(constellationLines);
|
|
908
1427
|
}
|
|
1428
|
+
if (cfg.groups) {
|
|
1429
|
+
for (const [bookId, chapters] of bookMap.entries()) {
|
|
1430
|
+
const bookNode = nodeById.get(bookId);
|
|
1431
|
+
if (!bookNode) continue;
|
|
1432
|
+
const bookName = bookNode.meta?.book || bookNode.label;
|
|
1433
|
+
const groupList = cfg.groups[bookName.toLowerCase()];
|
|
1434
|
+
if (groupList) {
|
|
1435
|
+
groupList.forEach((g, idx) => {
|
|
1436
|
+
const groupId = `G:${bookId}:${idx}`;
|
|
1437
|
+
let p = new THREE5.Vector3();
|
|
1438
|
+
if (cfg.arrangement && cfg.arrangement[groupId]) {
|
|
1439
|
+
const arr = cfg.arrangement[groupId];
|
|
1440
|
+
p.set(arr.position[0], arr.position[1], arr.position[2]);
|
|
1441
|
+
} else {
|
|
1442
|
+
const relevantChapters = chapters.filter((c) => {
|
|
1443
|
+
const ch = c.meta?.chapter;
|
|
1444
|
+
return ch >= g.start && ch <= g.end;
|
|
1445
|
+
});
|
|
1446
|
+
if (relevantChapters.length === 0) return;
|
|
1447
|
+
for (const c of relevantChapters) {
|
|
1448
|
+
p.add(getPosition(c));
|
|
1449
|
+
}
|
|
1450
|
+
p.divideScalar(relevantChapters.length);
|
|
1451
|
+
}
|
|
1452
|
+
const labelText = `${g.name} (${g.start}-${g.end})`;
|
|
1453
|
+
const texRes = createTextTexture(labelText, "#4fa4fa80");
|
|
1454
|
+
if (texRes) {
|
|
1455
|
+
const baseScale = 0.036;
|
|
1456
|
+
const size = new THREE5.Vector2(baseScale * texRes.aspect, baseScale);
|
|
1457
|
+
const mat = createSmartMaterial({
|
|
1458
|
+
uniforms: {
|
|
1459
|
+
uMap: { value: texRes.tex },
|
|
1460
|
+
uSize: { value: size },
|
|
1461
|
+
uAlpha: { value: 0 },
|
|
1462
|
+
uAngle: { value: 0 }
|
|
1463
|
+
},
|
|
1464
|
+
vertexShaderBody: `
|
|
1465
|
+
uniform vec2 uSize;
|
|
1466
|
+
uniform float uAngle;
|
|
1467
|
+
varying vec2 vUv;
|
|
1468
|
+
void main() {
|
|
1469
|
+
vUv = uv;
|
|
1470
|
+
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
1471
|
+
vec4 projected = smartProject(mvPos);
|
|
1472
|
+
|
|
1473
|
+
float c = cos(uAngle);
|
|
1474
|
+
float s = sin(uAngle);
|
|
1475
|
+
mat2 rot = mat2(c, -s, s, c);
|
|
1476
|
+
vec2 offset = rot * (position.xy * uSize);
|
|
1477
|
+
|
|
1478
|
+
projected.xy += offset / vec2(uAspect, 1.0);
|
|
1479
|
+
gl_Position = projected;
|
|
1480
|
+
}
|
|
1481
|
+
`,
|
|
1482
|
+
fragmentShader: `
|
|
1483
|
+
uniform sampler2D uMap;
|
|
1484
|
+
uniform float uAlpha;
|
|
1485
|
+
varying vec2 vUv;
|
|
1486
|
+
void main() {
|
|
1487
|
+
float mask = getMaskAlpha();
|
|
1488
|
+
if (mask < 0.01) discard;
|
|
1489
|
+
vec4 tex = texture2D(uMap, vUv);
|
|
1490
|
+
gl_FragColor = vec4(tex.rgb, tex.a * uAlpha * mask);
|
|
1491
|
+
}
|
|
1492
|
+
`,
|
|
1493
|
+
transparent: true,
|
|
1494
|
+
depthWrite: false,
|
|
1495
|
+
depthTest: true
|
|
1496
|
+
});
|
|
1497
|
+
const mesh = new THREE5.Mesh(new THREE5.PlaneGeometry(1, 1), mat);
|
|
1498
|
+
mesh.position.copy(p);
|
|
1499
|
+
mesh.scale.set(size.x, size.y, 1);
|
|
1500
|
+
mesh.frustumCulled = false;
|
|
1501
|
+
mesh.userData = { id: groupId };
|
|
1502
|
+
root.add(mesh);
|
|
1503
|
+
const node = {
|
|
1504
|
+
id: groupId,
|
|
1505
|
+
label: labelText,
|
|
1506
|
+
level: 2.5,
|
|
1507
|
+
// Special Level
|
|
1508
|
+
parent: bookId
|
|
1509
|
+
};
|
|
1510
|
+
dynamicLabels.push({ obj: mesh, node, initialScale: size.clone() });
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
909
1516
|
const boundaries = laidOut.meta?.divisionBoundaries ?? [];
|
|
910
1517
|
if (boundaries.length > 0) {
|
|
911
1518
|
const boundaryMat = createSmartMaterial({
|
|
912
|
-
uniforms: { color: { value: new
|
|
1519
|
+
uniforms: { color: { value: new THREE5.Color(5601177) } },
|
|
913
1520
|
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
1521
|
fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.10 * alphaMask); }`,
|
|
915
1522
|
transparent: true,
|
|
916
1523
|
depthWrite: false,
|
|
917
|
-
blending:
|
|
1524
|
+
blending: THREE5.AdditiveBlending
|
|
918
1525
|
});
|
|
919
|
-
const boundaryGeo = new
|
|
1526
|
+
const boundaryGeo = new THREE5.BufferGeometry();
|
|
920
1527
|
const bPoints = [];
|
|
921
1528
|
boundaries.forEach((angle) => {
|
|
922
1529
|
const steps = 32;
|
|
@@ -929,18 +1536,80 @@ function createEngine({
|
|
|
929
1536
|
bPoints.push(p2.x, p2.y, p2.z);
|
|
930
1537
|
}
|
|
931
1538
|
});
|
|
932
|
-
boundaryGeo.setAttribute("position", new
|
|
933
|
-
|
|
1539
|
+
boundaryGeo.setAttribute("position", new THREE5.Float32BufferAttribute(bPoints, 3));
|
|
1540
|
+
boundaryLines = new THREE5.LineSegments(boundaryGeo, boundaryMat);
|
|
934
1541
|
boundaryLines.frustumCulled = false;
|
|
935
1542
|
root.add(boundaryLines);
|
|
936
1543
|
}
|
|
1544
|
+
if (cfg.polygons) {
|
|
1545
|
+
const polyPoints = [];
|
|
1546
|
+
const rBase = layoutCfg.radius;
|
|
1547
|
+
for (const pts of Object.values(cfg.polygons)) {
|
|
1548
|
+
if (pts.length < 2) continue;
|
|
1549
|
+
for (let i = 0; i < pts.length; i++) {
|
|
1550
|
+
const p1_2d = pts[i];
|
|
1551
|
+
const p2_2d = pts[(i + 1) % pts.length];
|
|
1552
|
+
if (!p1_2d || !p2_2d) continue;
|
|
1553
|
+
const project2dTo3d = (p) => {
|
|
1554
|
+
const x = p[0];
|
|
1555
|
+
const y = p[1];
|
|
1556
|
+
const r_norm = Math.sqrt(x * x + y * y);
|
|
1557
|
+
const phi = Math.atan2(y, x);
|
|
1558
|
+
const theta = r_norm * (Math.PI / 2);
|
|
1559
|
+
return new THREE5.Vector3(
|
|
1560
|
+
Math.sin(theta) * Math.cos(phi),
|
|
1561
|
+
Math.cos(theta),
|
|
1562
|
+
Math.sin(theta) * Math.sin(phi)
|
|
1563
|
+
).multiplyScalar(rBase);
|
|
1564
|
+
};
|
|
1565
|
+
const v1 = project2dTo3d(p1_2d);
|
|
1566
|
+
const v2 = project2dTo3d(p2_2d);
|
|
1567
|
+
polyPoints.push(v1.x, v1.y, v1.z);
|
|
1568
|
+
polyPoints.push(v2.x, v2.y, v2.z);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
if (polyPoints.length > 0) {
|
|
1572
|
+
const polyGeo = new THREE5.BufferGeometry();
|
|
1573
|
+
polyGeo.setAttribute("position", new THREE5.Float32BufferAttribute(polyPoints, 3));
|
|
1574
|
+
const polyMat = createSmartMaterial({
|
|
1575
|
+
uniforms: { color: { value: new THREE5.Color(3718648) } },
|
|
1576
|
+
// Cyan-ish
|
|
1577
|
+
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; }`,
|
|
1578
|
+
fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.2 * alphaMask); }`,
|
|
1579
|
+
transparent: true,
|
|
1580
|
+
depthWrite: false,
|
|
1581
|
+
blending: THREE5.AdditiveBlending
|
|
1582
|
+
});
|
|
1583
|
+
const polyLines = new THREE5.LineSegments(polyGeo, polyMat);
|
|
1584
|
+
polyLines.frustumCulled = false;
|
|
1585
|
+
root.add(polyLines);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
937
1588
|
resize();
|
|
938
1589
|
}
|
|
939
1590
|
let lastData = void 0;
|
|
940
1591
|
let lastAdapter = void 0;
|
|
941
1592
|
let lastModel = void 0;
|
|
1593
|
+
let lastAppliedLon = void 0;
|
|
1594
|
+
let lastAppliedLat = void 0;
|
|
1595
|
+
let lastBackdropCount = void 0;
|
|
942
1596
|
function setConfig(cfg) {
|
|
943
1597
|
currentConfig = cfg;
|
|
1598
|
+
if (typeof cfg.camera?.lon === "number" && cfg.camera.lon !== lastAppliedLon) {
|
|
1599
|
+
state.lon = cfg.camera.lon;
|
|
1600
|
+
state.targetLon = cfg.camera.lon;
|
|
1601
|
+
lastAppliedLon = cfg.camera.lon;
|
|
1602
|
+
}
|
|
1603
|
+
if (typeof cfg.camera?.lat === "number" && cfg.camera.lat !== lastAppliedLat) {
|
|
1604
|
+
state.lat = cfg.camera.lat;
|
|
1605
|
+
state.targetLat = cfg.camera.lat;
|
|
1606
|
+
lastAppliedLat = cfg.camera.lat;
|
|
1607
|
+
}
|
|
1608
|
+
const desiredBackdropCount = typeof cfg.backdropStarsCount === "number" ? cfg.backdropStarsCount : 4e3;
|
|
1609
|
+
if (lastBackdropCount !== desiredBackdropCount) {
|
|
1610
|
+
createBackdropStars(desiredBackdropCount);
|
|
1611
|
+
lastBackdropCount = desiredBackdropCount;
|
|
1612
|
+
}
|
|
944
1613
|
let shouldRebuild = false;
|
|
945
1614
|
let model = cfg.model;
|
|
946
1615
|
if (!model && cfg.data && cfg.adapter) {
|
|
@@ -964,6 +1633,29 @@ function createEngine({
|
|
|
964
1633
|
} else if (cfg.arrangement && starPoints) {
|
|
965
1634
|
if (lastModel) buildFromModel(lastModel, cfg);
|
|
966
1635
|
}
|
|
1636
|
+
if (cfg.constellations) {
|
|
1637
|
+
constellationLayer.load(cfg.constellations, (id) => {
|
|
1638
|
+
if (cfg.arrangement && cfg.arrangement[id]) {
|
|
1639
|
+
const arr = cfg.arrangement[id];
|
|
1640
|
+
if (arr.position[2] === 0) {
|
|
1641
|
+
const x = arr.position[0];
|
|
1642
|
+
const y = arr.position[1];
|
|
1643
|
+
const radius = cfg.layout?.radius ?? 2e3;
|
|
1644
|
+
const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
|
|
1645
|
+
const phi = Math.atan2(y, x);
|
|
1646
|
+
const theta = r_norm * (Math.PI / 2);
|
|
1647
|
+
return new THREE5.Vector3(
|
|
1648
|
+
Math.sin(theta) * Math.cos(phi),
|
|
1649
|
+
Math.cos(theta),
|
|
1650
|
+
Math.sin(theta) * Math.sin(phi)
|
|
1651
|
+
).multiplyScalar(radius);
|
|
1652
|
+
}
|
|
1653
|
+
return new THREE5.Vector3(arr.position[0], arr.position[1], arr.position[2]);
|
|
1654
|
+
}
|
|
1655
|
+
const n = nodeById.get(id);
|
|
1656
|
+
return n ? getPosition(n) : null;
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
967
1659
|
}
|
|
968
1660
|
function setHandlers(next) {
|
|
969
1661
|
handlers = next;
|
|
@@ -971,22 +1663,25 @@ function createEngine({
|
|
|
971
1663
|
function getFullArrangement() {
|
|
972
1664
|
const arr = {};
|
|
973
1665
|
if (starPoints && starPoints.geometry.attributes.position) {
|
|
974
|
-
const
|
|
1666
|
+
const attr = starPoints.geometry.attributes.position;
|
|
975
1667
|
for (let i = 0; i < starIndexToId.length; i++) {
|
|
976
1668
|
const id = starIndexToId[i];
|
|
977
1669
|
if (id) {
|
|
978
|
-
const x =
|
|
979
|
-
const y =
|
|
980
|
-
const z =
|
|
981
|
-
|
|
982
|
-
arr[id] = { position: [x, y, z] };
|
|
983
|
-
}
|
|
1670
|
+
const x = attr.getX(i);
|
|
1671
|
+
const y = attr.getY(i);
|
|
1672
|
+
const z = attr.getZ(i);
|
|
1673
|
+
arr[id] = { position: [x, y, z] };
|
|
984
1674
|
}
|
|
985
1675
|
}
|
|
986
1676
|
}
|
|
987
1677
|
for (const item of dynamicLabels) {
|
|
1678
|
+
if (item.node.level === 3) continue;
|
|
988
1679
|
arr[item.node.id] = { position: [item.obj.position.x, item.obj.position.y, item.obj.position.z] };
|
|
989
1680
|
}
|
|
1681
|
+
for (const item of constellationLayer.getItems()) {
|
|
1682
|
+
arr[item.config.id] = { position: [item.mesh.position.x, item.mesh.position.y, item.mesh.position.z] };
|
|
1683
|
+
}
|
|
1684
|
+
Object.assign(arr, state.tempArrangement);
|
|
990
1685
|
return arr;
|
|
991
1686
|
}
|
|
992
1687
|
function pick(ev) {
|
|
@@ -995,16 +1690,18 @@ function createEngine({
|
|
|
995
1690
|
const mY = ev.clientY - rect.top;
|
|
996
1691
|
mouseNDC.x = mX / rect.width * 2 - 1;
|
|
997
1692
|
mouseNDC.y = -(mY / rect.height) * 2 + 1;
|
|
998
|
-
let closestLabel = null;
|
|
999
|
-
let minLabelDist = 40;
|
|
1000
1693
|
const uScale = globalUniforms.uScale.value;
|
|
1001
1694
|
const uAspect = camera.aspect;
|
|
1002
1695
|
const w = rect.width;
|
|
1003
1696
|
const h = rect.height;
|
|
1697
|
+
let closestLabel = null;
|
|
1698
|
+
let minLabelDist = 40;
|
|
1004
1699
|
for (const item of dynamicLabels) {
|
|
1005
1700
|
if (!item.obj.visible) continue;
|
|
1006
1701
|
const pWorld = item.obj.position;
|
|
1007
1702
|
const pProj = smartProjectJS(pWorld);
|
|
1703
|
+
const isBehind = globalUniforms.uBlend.value > 0.5 && pProj.z > 0.4 || globalUniforms.uBlend.value < 0.1 && pProj.z > -0.1;
|
|
1704
|
+
if (isBehind) continue;
|
|
1008
1705
|
const xNDC = pProj.x * uScale / uAspect;
|
|
1009
1706
|
const yNDC = pProj.y * uScale;
|
|
1010
1707
|
const sX = (xNDC * 0.5 + 0.5) * w;
|
|
@@ -1012,24 +1709,72 @@ function createEngine({
|
|
|
1012
1709
|
const dx = mX - sX;
|
|
1013
1710
|
const dy = mY - sY;
|
|
1014
1711
|
const d = Math.sqrt(dx * dx + dy * dy);
|
|
1015
|
-
|
|
1016
|
-
if (!isBehind && d < minLabelDist) {
|
|
1712
|
+
if (d < minLabelDist) {
|
|
1017
1713
|
minLabelDist = d;
|
|
1018
1714
|
closestLabel = item;
|
|
1019
1715
|
}
|
|
1020
1716
|
}
|
|
1021
|
-
if (closestLabel)
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
const
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1717
|
+
if (closestLabel) {
|
|
1718
|
+
return { type: "label", node: closestLabel.node, object: closestLabel.obj, point: closestLabel.obj.position.clone(), index: void 0 };
|
|
1719
|
+
}
|
|
1720
|
+
let closestConst = null;
|
|
1721
|
+
let minConstDist = Infinity;
|
|
1722
|
+
for (const item of constellationLayer.getItems()) {
|
|
1723
|
+
if (!item.mesh.visible) continue;
|
|
1724
|
+
const pWorld = item.mesh.position;
|
|
1725
|
+
const pProj = smartProjectJS(pWorld);
|
|
1726
|
+
const isBehind = globalUniforms.uBlend.value > 0.5 && pProj.z > 0.4 || globalUniforms.uBlend.value < 0.1 && pProj.z > -0.1;
|
|
1727
|
+
if (isBehind) continue;
|
|
1728
|
+
const uniforms = item.material.uniforms;
|
|
1729
|
+
if (!uniforms || !uniforms.uSize) continue;
|
|
1730
|
+
const uSize = uniforms.uSize.value;
|
|
1731
|
+
const uImgAspect = uniforms.uImgAspect.value;
|
|
1732
|
+
const uImgRotation = uniforms.uImgRotation.value;
|
|
1733
|
+
const dist = pWorld.length();
|
|
1734
|
+
if (dist < 1e-3) continue;
|
|
1735
|
+
const scale = uSize / dist * uScale;
|
|
1736
|
+
const halfH_px = scale / 2 * (h / 2);
|
|
1737
|
+
const halfW_px = halfH_px * uImgAspect;
|
|
1738
|
+
const xNDC = pProj.x * uScale / uAspect;
|
|
1739
|
+
const yNDC = pProj.y * uScale;
|
|
1740
|
+
const sX = (xNDC * 0.5 + 0.5) * w;
|
|
1741
|
+
const sY = (-yNDC * 0.5 + 0.5) * h;
|
|
1742
|
+
const dx = mX - sX;
|
|
1743
|
+
const dy = mY - sY;
|
|
1744
|
+
const dy_cart = -dy;
|
|
1745
|
+
const cr = Math.cos(-uImgRotation);
|
|
1746
|
+
const sr = Math.sin(-uImgRotation);
|
|
1747
|
+
const localX = dx * cr - dy_cart * sr;
|
|
1748
|
+
const localY = dx * sr + dy_cart * cr;
|
|
1749
|
+
if (Math.abs(localX) < halfW_px * 1.2 && Math.abs(localY) < halfH_px * 1.2) {
|
|
1750
|
+
const d = Math.sqrt(dx * dx + dy * dy);
|
|
1751
|
+
if (!closestConst || d < minConstDist) {
|
|
1752
|
+
minConstDist = d;
|
|
1753
|
+
closestConst = item;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
if (closestConst) {
|
|
1758
|
+
const fakeNode = {
|
|
1759
|
+
id: closestConst.config.id,
|
|
1760
|
+
label: closestConst.config.title,
|
|
1761
|
+
level: -1
|
|
1762
|
+
};
|
|
1763
|
+
return { type: "constellation", node: fakeNode, object: closestConst.mesh, point: closestConst.mesh.position.clone(), index: void 0 };
|
|
1764
|
+
}
|
|
1765
|
+
if (starPoints) {
|
|
1766
|
+
const worldDir = getMouseWorldVector(mX, mY, rect.width, rect.height);
|
|
1767
|
+
raycaster.ray.origin.set(0, 0, 0);
|
|
1768
|
+
raycaster.ray.direction.copy(worldDir);
|
|
1769
|
+
raycaster.params.Points.threshold = 5 * (state.fov / 60);
|
|
1770
|
+
const hits = raycaster.intersectObject(starPoints, false);
|
|
1771
|
+
const pointHit = hits[0];
|
|
1772
|
+
if (pointHit && pointHit.index !== void 0) {
|
|
1773
|
+
const id = starIndexToId[pointHit.index];
|
|
1774
|
+
if (id) {
|
|
1775
|
+
const node = nodeById.get(id);
|
|
1776
|
+
if (node) return { type: "star", node, index: pointHit.index, point: pointHit.point, object: void 0 };
|
|
1777
|
+
}
|
|
1033
1778
|
}
|
|
1034
1779
|
}
|
|
1035
1780
|
return void 0;
|
|
@@ -1057,21 +1802,25 @@ function createEngine({
|
|
|
1057
1802
|
if (starId) {
|
|
1058
1803
|
const starNode = nodeById.get(starId);
|
|
1059
1804
|
if (starNode && starNode.parent === bookId) {
|
|
1060
|
-
children.push({ index: i, initialPos: new
|
|
1805
|
+
children.push({ index: i, initialPos: new THREE5.Vector3(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]) });
|
|
1061
1806
|
}
|
|
1062
1807
|
}
|
|
1063
1808
|
}
|
|
1064
1809
|
}
|
|
1065
1810
|
state.draggedGroup = { labelInitialPos: hit.object.position.clone(), children };
|
|
1066
1811
|
state.draggedStarIndex = -1;
|
|
1812
|
+
} else if (hit.type === "constellation") {
|
|
1813
|
+
state.draggedGroup = null;
|
|
1814
|
+
state.draggedStarIndex = -1;
|
|
1067
1815
|
}
|
|
1068
|
-
return;
|
|
1069
1816
|
}
|
|
1817
|
+
return;
|
|
1070
1818
|
}
|
|
1071
1819
|
state.dragMode = "camera";
|
|
1072
1820
|
state.isDragging = true;
|
|
1073
1821
|
state.velocityX = 0;
|
|
1074
1822
|
state.velocityY = 0;
|
|
1823
|
+
state.tempArrangement = {};
|
|
1075
1824
|
document.body.style.cursor = "grabbing";
|
|
1076
1825
|
}
|
|
1077
1826
|
function onMouseMove(e) {
|
|
@@ -1092,16 +1841,29 @@ function createEngine({
|
|
|
1092
1841
|
} else if (state.draggedGroup && state.draggedNodeId) {
|
|
1093
1842
|
const group = state.draggedGroup;
|
|
1094
1843
|
const item = dynamicLabels.find((l) => l.node.id === state.draggedNodeId);
|
|
1095
|
-
if (item)
|
|
1844
|
+
if (item) {
|
|
1845
|
+
item.obj.position.copy(newPos);
|
|
1846
|
+
state.tempArrangement[item.node.id] = { position: [newPos.x, newPos.y, newPos.z] };
|
|
1847
|
+
} else if (state.draggedNodeId) {
|
|
1848
|
+
const cItem = constellationLayer.getItems().find((c) => c.config.id === state.draggedNodeId);
|
|
1849
|
+
if (cItem) {
|
|
1850
|
+
cItem.mesh.position.copy(newPos);
|
|
1851
|
+
state.tempArrangement[state.draggedNodeId] = { position: [newPos.x, newPos.y, newPos.z] };
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1096
1854
|
const vStart = group.labelInitialPos.clone().normalize();
|
|
1097
1855
|
const vEnd = newPos.clone().normalize();
|
|
1098
|
-
const q = new
|
|
1856
|
+
const q = new THREE5.Quaternion().setFromUnitVectors(vStart, vEnd);
|
|
1099
1857
|
if (starPoints && group.children.length > 0) {
|
|
1100
1858
|
const attr = starPoints.geometry.attributes.position;
|
|
1101
|
-
const tempVec = new
|
|
1859
|
+
const tempVec = new THREE5.Vector3();
|
|
1102
1860
|
for (const child of group.children) {
|
|
1103
1861
|
tempVec.copy(child.initialPos).applyQuaternion(q);
|
|
1104
1862
|
attr.setXYZ(child.index, tempVec.x, tempVec.y, tempVec.z);
|
|
1863
|
+
const id = starIndexToId[child.index];
|
|
1864
|
+
if (id) {
|
|
1865
|
+
state.tempArrangement[id] = { position: [tempVec.x, tempVec.y, tempVec.z] };
|
|
1866
|
+
}
|
|
1105
1867
|
}
|
|
1106
1868
|
attr.needsUpdate = true;
|
|
1107
1869
|
}
|
|
@@ -1121,9 +1883,30 @@ function createEngine({
|
|
|
1121
1883
|
state.lat = state.targetLat;
|
|
1122
1884
|
} else {
|
|
1123
1885
|
const hit = pick(e);
|
|
1886
|
+
if (hit && hit.type === "star") {
|
|
1887
|
+
if (currentHoverNodeId !== hit.node.id) {
|
|
1888
|
+
currentHoverNodeId = hit.node.id;
|
|
1889
|
+
const res = createTextTexture(hit.node.label, "#ffd700");
|
|
1890
|
+
if (res) {
|
|
1891
|
+
hoverLabelMat.uniforms.uMap.value = res.tex;
|
|
1892
|
+
const baseScale = 0.03;
|
|
1893
|
+
const size = new THREE5.Vector2(baseScale * res.aspect, baseScale);
|
|
1894
|
+
hoverLabelMat.uniforms.uSize.value = size;
|
|
1895
|
+
hoverLabelMesh.scale.set(size.x, size.y, 1);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
hoverLabelMesh.position.copy(hit.point);
|
|
1899
|
+
hoverLabelMat.uniforms.uAlpha.value = 1;
|
|
1900
|
+
hoverLabelMesh.visible = true;
|
|
1901
|
+
} else {
|
|
1902
|
+
currentHoverNodeId = null;
|
|
1903
|
+
hoverLabelMat.uniforms.uAlpha.value = 0;
|
|
1904
|
+
hoverLabelMesh.visible = false;
|
|
1905
|
+
}
|
|
1124
1906
|
if (hit?.node.id !== handlers._lastHoverId) {
|
|
1125
1907
|
handlers._lastHoverId = hit?.node.id;
|
|
1126
1908
|
handlers.onHover?.(hit?.node);
|
|
1909
|
+
constellationLayer.setHovered(hit?.node.id ?? null);
|
|
1127
1910
|
}
|
|
1128
1911
|
document.body.style.cursor = hit ? currentConfig?.editable ? "crosshair" : "pointer" : "default";
|
|
1129
1912
|
}
|
|
@@ -1143,7 +1926,14 @@ function createEngine({
|
|
|
1143
1926
|
document.body.style.cursor = "default";
|
|
1144
1927
|
} else {
|
|
1145
1928
|
const hit = pick(e);
|
|
1146
|
-
if (hit)
|
|
1929
|
+
if (hit) {
|
|
1930
|
+
handlers.onSelect?.(hit.node);
|
|
1931
|
+
constellationLayer.setFocused(hit.node.id);
|
|
1932
|
+
if (hit.node.level === 2) setFocusedBook(hit.node.id);
|
|
1933
|
+
else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
|
|
1934
|
+
} else {
|
|
1935
|
+
setFocusedBook(null);
|
|
1936
|
+
}
|
|
1147
1937
|
}
|
|
1148
1938
|
}
|
|
1149
1939
|
function onWheel(e) {
|
|
@@ -1154,25 +1944,26 @@ function createEngine({
|
|
|
1154
1944
|
const zoomSpeed = 1e-3 * state.fov;
|
|
1155
1945
|
state.fov += e.deltaY * zoomSpeed;
|
|
1156
1946
|
state.fov = Math.max(ENGINE_CONFIG.minFov, Math.min(ENGINE_CONFIG.maxFov, state.fov));
|
|
1947
|
+
handlers.onFovChange?.(state.fov);
|
|
1157
1948
|
updateUniforms();
|
|
1158
1949
|
const vAfter = getMouseViewVector(state.fov, aspect);
|
|
1159
|
-
const quaternion = new
|
|
1950
|
+
const quaternion = new THREE5.Quaternion().setFromUnitVectors(vAfter, vBefore);
|
|
1160
1951
|
const y = Math.sin(state.lat);
|
|
1161
1952
|
const r = Math.cos(state.lat);
|
|
1162
1953
|
const x = r * Math.sin(state.lon);
|
|
1163
1954
|
const z = -r * Math.cos(state.lon);
|
|
1164
|
-
const currentLook = new
|
|
1955
|
+
const currentLook = new THREE5.Vector3(x, y, z);
|
|
1165
1956
|
const camForward = currentLook.clone().normalize();
|
|
1166
1957
|
const camUp = camera.up.clone();
|
|
1167
|
-
const camRight = new
|
|
1168
|
-
const camUpOrtho = new
|
|
1169
|
-
const mat = new
|
|
1170
|
-
const qOld = new
|
|
1958
|
+
const camRight = new THREE5.Vector3().crossVectors(camForward, camUp).normalize();
|
|
1959
|
+
const camUpOrtho = new THREE5.Vector3().crossVectors(camRight, camForward).normalize();
|
|
1960
|
+
const mat = new THREE5.Matrix4().makeBasis(camRight, camUpOrtho, camForward.clone().negate());
|
|
1961
|
+
const qOld = new THREE5.Quaternion().setFromRotationMatrix(mat);
|
|
1171
1962
|
const qNew = qOld.clone().multiply(quaternion);
|
|
1172
|
-
const newForward = new
|
|
1963
|
+
const newForward = new THREE5.Vector3(0, 0, -1).applyQuaternion(qNew);
|
|
1173
1964
|
state.lat = Math.asin(Math.max(-0.999, Math.min(0.999, newForward.y)));
|
|
1174
1965
|
state.lon = Math.atan2(newForward.x, -newForward.z);
|
|
1175
|
-
const newUp = new
|
|
1966
|
+
const newUp = new THREE5.Vector3(0, 1, 0).applyQuaternion(qNew);
|
|
1176
1967
|
camera.up.copy(newUp);
|
|
1177
1968
|
if (e.deltaY > 0 && state.fov > ENGINE_CONFIG.zenithStartFov) {
|
|
1178
1969
|
const range = ENGINE_CONFIG.maxFov - ENGINE_CONFIG.zenithStartFov;
|
|
@@ -1213,79 +2004,205 @@ function createEngine({
|
|
|
1213
2004
|
function tick() {
|
|
1214
2005
|
if (!running) return;
|
|
1215
2006
|
raf = requestAnimationFrame(tick);
|
|
1216
|
-
|
|
2007
|
+
const now = performance.now();
|
|
2008
|
+
globalUniforms.uTime.value = now / 1e3;
|
|
2009
|
+
let activeId = null;
|
|
2010
|
+
if (focusedBookId) {
|
|
2011
|
+
activeId = focusedBookId;
|
|
2012
|
+
} else if (hoveredBookId) {
|
|
2013
|
+
const lastExit = hoverCooldowns.get(hoveredBookId) || 0;
|
|
2014
|
+
if (now - lastExit > COOLDOWN_MS) {
|
|
2015
|
+
activeId = hoveredBookId;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
const targetStrength = orderRevealEnabled && activeId ? 1 : 0;
|
|
2019
|
+
orderRevealStrength = mix(orderRevealStrength, targetStrength, 0.1);
|
|
2020
|
+
if (orderRevealStrength > 1e-3 || targetStrength > 0) {
|
|
2021
|
+
if (activeId && bookIdToIndex.has(activeId)) {
|
|
2022
|
+
activeBookIndex = bookIdToIndex.get(activeId);
|
|
2023
|
+
}
|
|
2024
|
+
if (starPoints && starPoints.material) {
|
|
2025
|
+
const m = starPoints.material;
|
|
2026
|
+
if (m.uniforms.uActiveBookIndex) m.uniforms.uActiveBookIndex.value = activeBookIndex;
|
|
2027
|
+
if (m.uniforms.uOrderRevealStrength) m.uniforms.uOrderRevealStrength.value = orderRevealStrength;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
let panX = 0;
|
|
2031
|
+
let panY = 0;
|
|
2032
|
+
if (!state.isDragging && isMouseInWindow && !currentConfig?.editable) {
|
|
1217
2033
|
const t = ENGINE_CONFIG.edgePanThreshold;
|
|
1218
|
-
const
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
state.targetLat = state.lat;
|
|
2034
|
+
const inZoneX = mouseNDC.x < -1 + t || mouseNDC.x > 1 - t;
|
|
2035
|
+
const inZoneY = mouseNDC.y < -1 + t || mouseNDC.y > 1 - t;
|
|
2036
|
+
if (inZoneX || inZoneY) {
|
|
2037
|
+
if (edgeHoverStart === 0) edgeHoverStart = performance.now();
|
|
2038
|
+
if (performance.now() - edgeHoverStart > ENGINE_CONFIG.edgePanDelay) {
|
|
2039
|
+
const speedBase = ENGINE_CONFIG.edgePanMaxSpeed * (state.fov / ENGINE_CONFIG.defaultFov);
|
|
2040
|
+
if (mouseNDC.x < -1 + t) {
|
|
2041
|
+
const s = (-1 + t - mouseNDC.x) / t;
|
|
2042
|
+
panX = -s * s * speedBase;
|
|
2043
|
+
} else if (mouseNDC.x > 1 - t) {
|
|
2044
|
+
const s = (mouseNDC.x - (1 - t)) / t;
|
|
2045
|
+
panX = s * s * speedBase;
|
|
2046
|
+
}
|
|
2047
|
+
if (mouseNDC.y < -1 + t) {
|
|
2048
|
+
const s = (-1 + t - mouseNDC.y) / t;
|
|
2049
|
+
panY = -s * s * speedBase;
|
|
2050
|
+
} else if (mouseNDC.y > 1 - t) {
|
|
2051
|
+
const s = (mouseNDC.y - (1 - t)) / t;
|
|
2052
|
+
panY = s * s * speedBase;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
1240
2055
|
} else {
|
|
1241
|
-
|
|
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;
|
|
2056
|
+
edgeHoverStart = 0;
|
|
1247
2057
|
}
|
|
2058
|
+
} else {
|
|
2059
|
+
edgeHoverStart = 0;
|
|
2060
|
+
}
|
|
2061
|
+
if (Math.abs(panX) > 0 || Math.abs(panY) > 0) {
|
|
2062
|
+
state.lon += panX;
|
|
2063
|
+
state.lat += panY;
|
|
2064
|
+
state.targetLon = state.lon;
|
|
2065
|
+
state.targetLat = state.lat;
|
|
1248
2066
|
} else if (!state.isDragging) {
|
|
1249
2067
|
state.lon += state.velocityX;
|
|
1250
2068
|
state.lat += state.velocityY;
|
|
1251
2069
|
state.velocityX *= ENGINE_CONFIG.inertiaDamping;
|
|
1252
2070
|
state.velocityY *= ENGINE_CONFIG.inertiaDamping;
|
|
2071
|
+
if (Math.abs(state.velocityX) < 1e-6) state.velocityX = 0;
|
|
2072
|
+
if (Math.abs(state.velocityY) < 1e-6) state.velocityY = 0;
|
|
1253
2073
|
}
|
|
1254
2074
|
state.lat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.lat));
|
|
1255
2075
|
const y = Math.sin(state.lat);
|
|
1256
2076
|
const r = Math.cos(state.lat);
|
|
1257
2077
|
const x = r * Math.sin(state.lon);
|
|
1258
2078
|
const z = -r * Math.cos(state.lon);
|
|
1259
|
-
const target = new
|
|
1260
|
-
const idealUp = new
|
|
2079
|
+
const target = new THREE5.Vector3(x, y, z);
|
|
2080
|
+
const idealUp = new THREE5.Vector3(-Math.sin(state.lat) * Math.sin(state.lon), Math.cos(state.lat), Math.sin(state.lat) * Math.cos(state.lon)).normalize();
|
|
1261
2081
|
camera.up.lerp(idealUp, ENGINE_CONFIG.horizonLockStrength);
|
|
1262
2082
|
camera.up.normalize();
|
|
1263
2083
|
camera.lookAt(target);
|
|
2084
|
+
camera.updateMatrixWorld();
|
|
2085
|
+
camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
|
|
1264
2086
|
updateUniforms();
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
const
|
|
1269
|
-
const
|
|
2087
|
+
constellationLayer.update(state.fov, currentConfig?.showConstellationArt ?? false);
|
|
2088
|
+
backdropGroup.visible = currentConfig?.showBackdropStars ?? true;
|
|
2089
|
+
if (atmosphereMesh) atmosphereMesh.visible = currentConfig?.showAtmosphere ?? false;
|
|
2090
|
+
const DIVISION_THRESHOLD = 60;
|
|
2091
|
+
const showDivisions = state.fov > DIVISION_THRESHOLD;
|
|
2092
|
+
if (constellationLines) {
|
|
2093
|
+
constellationLines.visible = currentConfig?.showConstellationLines ?? false;
|
|
2094
|
+
}
|
|
2095
|
+
if (boundaryLines) {
|
|
2096
|
+
boundaryLines.visible = currentConfig?.showDivisionBoundaries ?? false;
|
|
2097
|
+
}
|
|
2098
|
+
const rect = renderer.domElement.getBoundingClientRect();
|
|
2099
|
+
const screenW = rect.width;
|
|
2100
|
+
const screenH = rect.height;
|
|
2101
|
+
const aspect = screenW / screenH;
|
|
2102
|
+
const labelsToCheck = [];
|
|
2103
|
+
const occupied = [];
|
|
2104
|
+
function isOverlapping(x2, y2, w, h) {
|
|
2105
|
+
for (const r2 of occupied) {
|
|
2106
|
+
if (x2 < r2.x + r2.w && x2 + w > r2.x && y2 < r2.y + r2.h && y2 + h > r2.y) return true;
|
|
2107
|
+
}
|
|
2108
|
+
return false;
|
|
2109
|
+
}
|
|
2110
|
+
const showBookLabels = currentConfig?.showBookLabels === true;
|
|
2111
|
+
const showDivisionLabels = currentConfig?.showDivisionLabels === true;
|
|
2112
|
+
const showChapterLabels = currentConfig?.showChapterLabels === true;
|
|
2113
|
+
const showGroupLabels = currentConfig?.showGroupLabels === true;
|
|
2114
|
+
const showBooks = state.fov < 120;
|
|
2115
|
+
const showChapters = state.fov < 70;
|
|
1270
2116
|
for (const item of dynamicLabels) {
|
|
1271
2117
|
const uniforms = item.obj.material.uniforms;
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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);
|
|
2118
|
+
const level = item.node.level;
|
|
2119
|
+
let isEnabled = false;
|
|
2120
|
+
if (level === 2 && showBookLabels) isEnabled = true;
|
|
2121
|
+
else if (level === 1 && showDivisionLabels) isEnabled = true;
|
|
2122
|
+
else if (level === 3 && showChapterLabels) isEnabled = true;
|
|
2123
|
+
else if (level === 2.5 && showGroupLabels) isEnabled = true;
|
|
2124
|
+
if (!isEnabled) {
|
|
2125
|
+
uniforms.uAlpha.value = THREE5.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
1287
2126
|
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
2127
|
+
continue;
|
|
1288
2128
|
}
|
|
2129
|
+
const pWorld = item.obj.position;
|
|
2130
|
+
const pProj = smartProjectJS(pWorld);
|
|
2131
|
+
if (pProj.z > 0.2) {
|
|
2132
|
+
uniforms.uAlpha.value = THREE5.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
2133
|
+
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
2134
|
+
continue;
|
|
2135
|
+
}
|
|
2136
|
+
if (level === 2 && !showBooks && item.node.id !== state.draggedNodeId) {
|
|
2137
|
+
uniforms.uAlpha.value = THREE5.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
2138
|
+
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
2139
|
+
continue;
|
|
2140
|
+
}
|
|
2141
|
+
if ((level === 3 || level === 2.5) && !showChapters && item.node.id !== state.draggedNodeId) {
|
|
2142
|
+
uniforms.uAlpha.value = THREE5.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
2143
|
+
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
2144
|
+
continue;
|
|
2145
|
+
}
|
|
2146
|
+
const ndcX = pProj.x * globalUniforms.uScale.value / aspect;
|
|
2147
|
+
const ndcY = pProj.y * globalUniforms.uScale.value;
|
|
2148
|
+
const sX = (ndcX * 0.5 + 0.5) * screenW;
|
|
2149
|
+
const sY = (-ndcY * 0.5 + 0.5) * screenH;
|
|
2150
|
+
const size = uniforms.uSize.value;
|
|
2151
|
+
const pixelH = size.y * screenH * 0.8;
|
|
2152
|
+
const pixelW = size.x * screenH * 0.8;
|
|
2153
|
+
labelsToCheck.push({ item, sX, sY, w: pixelW, h: pixelH, uniforms, level, ndcX, ndcY });
|
|
2154
|
+
}
|
|
2155
|
+
const hoverId = handlers._lastHoverId;
|
|
2156
|
+
const selectedId = state.draggedNodeId;
|
|
2157
|
+
labelsToCheck.sort((a, b) => {
|
|
2158
|
+
const getScore = (l) => {
|
|
2159
|
+
if (l.item.node.id === selectedId) return 10;
|
|
2160
|
+
if (l.item.node.id === hoverId) return 9;
|
|
2161
|
+
const level = l.level;
|
|
2162
|
+
if (level === 2) return 5;
|
|
2163
|
+
if (level === 1) return showDivisions ? 6 : 1;
|
|
2164
|
+
return 0;
|
|
2165
|
+
};
|
|
2166
|
+
return getScore(b) - getScore(a);
|
|
2167
|
+
});
|
|
2168
|
+
for (const l of labelsToCheck) {
|
|
2169
|
+
let target2 = 0;
|
|
2170
|
+
const isSpecial = l.item.node.id === selectedId || l.item.node.id === hoverId;
|
|
2171
|
+
if (l.level === 1) {
|
|
2172
|
+
let rot = 0;
|
|
2173
|
+
const blend = globalUniforms.uBlend.value;
|
|
2174
|
+
if (blend > 0.5) {
|
|
2175
|
+
const dx = l.sX - screenW / 2;
|
|
2176
|
+
const dy = l.sY - screenH / 2;
|
|
2177
|
+
rot = Math.atan2(-dy, -dx) - Math.PI / 2;
|
|
2178
|
+
}
|
|
2179
|
+
l.uniforms.uAngle.value = THREE5.MathUtils.lerp(l.uniforms.uAngle.value, rot, 0.1);
|
|
2180
|
+
}
|
|
2181
|
+
if (l.level === 2) {
|
|
2182
|
+
if (showBooks || isSpecial) {
|
|
2183
|
+
target2 = 1;
|
|
2184
|
+
occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
|
|
2185
|
+
}
|
|
2186
|
+
} else if (l.level === 1) {
|
|
2187
|
+
if (showDivisions || isSpecial) {
|
|
2188
|
+
const pad = -5;
|
|
2189
|
+
if (!isOverlapping(l.sX - l.w / 2 - pad, l.sY - l.h / 2 - pad, l.w + pad * 2, l.h + pad * 2)) {
|
|
2190
|
+
target2 = 1;
|
|
2191
|
+
occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
} else if (l.level === 2.5 || l.level === 3) {
|
|
2195
|
+
if (showChapters || isSpecial) {
|
|
2196
|
+
target2 = 1;
|
|
2197
|
+
if (!isSpecial) {
|
|
2198
|
+
const dist = Math.sqrt(l.ndcX * l.ndcX + l.ndcY * l.ndcY);
|
|
2199
|
+
const focusFade = 1 - THREE5.MathUtils.smoothstep(0.4, 0.7, dist);
|
|
2200
|
+
target2 *= focusFade;
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
l.uniforms.uAlpha.value = THREE5.MathUtils.lerp(l.uniforms.uAlpha.value, target2, 0.1);
|
|
2205
|
+
l.item.obj.visible = l.uniforms.uAlpha.value > 0.01;
|
|
1289
2206
|
}
|
|
1290
2207
|
renderer.render(scene, camera);
|
|
1291
2208
|
}
|
|
@@ -1301,16 +2218,31 @@ function createEngine({
|
|
|
1301
2218
|
}
|
|
1302
2219
|
function dispose() {
|
|
1303
2220
|
stop();
|
|
2221
|
+
constellationLayer.dispose();
|
|
1304
2222
|
renderer.dispose();
|
|
1305
2223
|
renderer.domElement.remove();
|
|
1306
2224
|
}
|
|
1307
|
-
|
|
2225
|
+
function setHoveredBook(id) {
|
|
2226
|
+
if (id === hoveredBookId) return;
|
|
2227
|
+
if (hoveredBookId) {
|
|
2228
|
+
hoverCooldowns.set(hoveredBookId, performance.now());
|
|
2229
|
+
}
|
|
2230
|
+
hoveredBookId = id;
|
|
2231
|
+
}
|
|
2232
|
+
function setFocusedBook(id) {
|
|
2233
|
+
focusedBookId = id;
|
|
2234
|
+
}
|
|
2235
|
+
function setOrderRevealEnabled(enabled) {
|
|
2236
|
+
orderRevealEnabled = enabled;
|
|
2237
|
+
}
|
|
2238
|
+
return { setConfig, start, stop, dispose, setHandlers, getFullArrangement, setHoveredBook, setFocusedBook, setOrderRevealEnabled };
|
|
1308
2239
|
}
|
|
1309
|
-
var ENGINE_CONFIG;
|
|
2240
|
+
var ENGINE_CONFIG, ORDER_REVEAL_CONFIG;
|
|
1310
2241
|
var init_createEngine = __esm({
|
|
1311
2242
|
"src/engine/createEngine.ts"() {
|
|
1312
2243
|
init_layout();
|
|
1313
2244
|
init_materials();
|
|
2245
|
+
init_ConstellationArtworkLayer();
|
|
1314
2246
|
ENGINE_CONFIG = {
|
|
1315
2247
|
minFov: 10,
|
|
1316
2248
|
maxFov: 165,
|
|
@@ -1323,16 +2255,26 @@ var init_createEngine = __esm({
|
|
|
1323
2255
|
zenithStrength: 0.02,
|
|
1324
2256
|
horizonLockStrength: 0.05,
|
|
1325
2257
|
edgePanThreshold: 0.15,
|
|
1326
|
-
edgePanMaxSpeed: 0.02
|
|
2258
|
+
edgePanMaxSpeed: 0.02,
|
|
2259
|
+
edgePanDelay: 250
|
|
2260
|
+
};
|
|
2261
|
+
ORDER_REVEAL_CONFIG = {
|
|
2262
|
+
globalDim: 0.85,
|
|
2263
|
+
pulseAmplitude: 0.6,
|
|
2264
|
+
pulseDuration: 2,
|
|
2265
|
+
delayPerChapter: 0.1
|
|
1327
2266
|
};
|
|
1328
2267
|
}
|
|
1329
2268
|
});
|
|
1330
2269
|
var StarMap = forwardRef(
|
|
1331
|
-
({ config, className, onSelect, onHover, onArrangementChange }, ref) => {
|
|
2270
|
+
({ config, className, onSelect, onHover, onArrangementChange, onFovChange }, ref) => {
|
|
1332
2271
|
const containerRef = useRef(null);
|
|
1333
2272
|
const engineRef = useRef(null);
|
|
1334
2273
|
useImperativeHandle(ref, () => ({
|
|
1335
|
-
getFullArrangement: () => engineRef.current?.getFullArrangement?.()
|
|
2274
|
+
getFullArrangement: () => engineRef.current?.getFullArrangement?.(),
|
|
2275
|
+
setHoveredBook: (id) => engineRef.current?.setHoveredBook?.(id),
|
|
2276
|
+
setFocusedBook: (id) => engineRef.current?.setFocusedBook?.(id),
|
|
2277
|
+
setOrderRevealEnabled: (enabled) => engineRef.current?.setOrderRevealEnabled?.(enabled)
|
|
1336
2278
|
}));
|
|
1337
2279
|
useEffect(() => {
|
|
1338
2280
|
let disposed = false;
|
|
@@ -1344,7 +2286,8 @@ var StarMap = forwardRef(
|
|
|
1344
2286
|
container: containerRef.current,
|
|
1345
2287
|
onSelect,
|
|
1346
2288
|
onHover,
|
|
1347
|
-
onArrangementChange
|
|
2289
|
+
onArrangementChange,
|
|
2290
|
+
onFovChange
|
|
1348
2291
|
});
|
|
1349
2292
|
engineRef.current.setConfig(config);
|
|
1350
2293
|
engineRef.current.start();
|
|
@@ -1360,8 +2303,8 @@ var StarMap = forwardRef(
|
|
|
1360
2303
|
engineRef.current?.setConfig?.(config);
|
|
1361
2304
|
}, [config]);
|
|
1362
2305
|
useEffect(() => {
|
|
1363
|
-
engineRef.current?.setHandlers?.({ onSelect, onHover, onArrangementChange });
|
|
1364
|
-
}, [onSelect, onHover, onArrangementChange]);
|
|
2306
|
+
engineRef.current?.setHandlers?.({ onSelect, onHover, onArrangementChange, onFovChange });
|
|
2307
|
+
}, [onSelect, onHover, onArrangementChange, onFovChange]);
|
|
1365
2308
|
return /* @__PURE__ */ jsx("div", { ref: containerRef, className, style: { width: "100%", height: "100%" } });
|
|
1366
2309
|
}
|
|
1367
2310
|
);
|
|
@@ -1390,10 +2333,11 @@ function bibleToSceneModel(data) {
|
|
|
1390
2333
|
});
|
|
1391
2334
|
links.push({ source: did, target: tid });
|
|
1392
2335
|
for (const b of d.books) {
|
|
2336
|
+
const bookLabel = b.name;
|
|
1393
2337
|
const bid = id.book(b.key);
|
|
1394
2338
|
nodes.push({
|
|
1395
2339
|
id: bid,
|
|
1396
|
-
label:
|
|
2340
|
+
label: bookLabel,
|
|
1397
2341
|
level: 2,
|
|
1398
2342
|
parent: did,
|
|
1399
2343
|
meta: { testament: t.name, division: d.name, bookKey: b.key, book: b.name }
|
|
@@ -1405,7 +2349,7 @@ function bibleToSceneModel(data) {
|
|
|
1405
2349
|
const cid = id.chapter(b.key, chapterNum);
|
|
1406
2350
|
nodes.push({
|
|
1407
2351
|
id: cid,
|
|
1408
|
-
label: `${
|
|
2352
|
+
label: `${bookLabel} ${chapterNum}`,
|
|
1409
2353
|
level: 3,
|
|
1410
2354
|
parent: bid,
|
|
1411
2355
|
weight: verseCounts[i],
|
|
@@ -30468,7 +31412,145 @@ var default_stars_default = {
|
|
|
30468
31412
|
}
|
|
30469
31413
|
]
|
|
30470
31414
|
};
|
|
31415
|
+
var defaultGenerateOptions = {
|
|
31416
|
+
seed: 12345,
|
|
31417
|
+
discRadius: 2e3,
|
|
31418
|
+
milkyWayEnabled: true,
|
|
31419
|
+
milkyWayAngle: 60,
|
|
31420
|
+
milkyWayWidth: 0.3,
|
|
31421
|
+
// Width in dot-product space
|
|
31422
|
+
milkyWayStrength: 0.7,
|
|
31423
|
+
noiseScale: 2,
|
|
31424
|
+
noiseStrength: 0.4,
|
|
31425
|
+
clusterSpread: 0.08
|
|
31426
|
+
// Radians approx
|
|
31427
|
+
};
|
|
31428
|
+
var RNG = class {
|
|
31429
|
+
seed;
|
|
31430
|
+
constructor(seed) {
|
|
31431
|
+
this.seed = seed;
|
|
31432
|
+
}
|
|
31433
|
+
// Returns 0..1
|
|
31434
|
+
next() {
|
|
31435
|
+
this.seed = (this.seed * 9301 + 49297) % 233280;
|
|
31436
|
+
return this.seed / 233280;
|
|
31437
|
+
}
|
|
31438
|
+
// Returns range [min, max)
|
|
31439
|
+
range(min, max) {
|
|
31440
|
+
return min + this.next() * (max - min);
|
|
31441
|
+
}
|
|
31442
|
+
// Uniform random on upper hemisphere (y > 0)
|
|
31443
|
+
randomOnSphere() {
|
|
31444
|
+
const y = this.next();
|
|
31445
|
+
const theta = 2 * Math.PI * this.next();
|
|
31446
|
+
const r = Math.sqrt(1 - y * y);
|
|
31447
|
+
const x = r * Math.cos(theta);
|
|
31448
|
+
const z = r * Math.sin(theta);
|
|
31449
|
+
return new THREE5.Vector3(x, y, z);
|
|
31450
|
+
}
|
|
31451
|
+
};
|
|
31452
|
+
function simpleNoise3D(v, scale) {
|
|
31453
|
+
const s = scale;
|
|
31454
|
+
return (Math.sin(v.x * s) + Math.sin(v.y * s * 1.3) + Math.sin(v.z * s * 1.7) + Math.sin(v.x * s * 2.1 + v.y * s * 2.1) * 0.5) / 3.5;
|
|
31455
|
+
}
|
|
31456
|
+
function getDensity(v, opts, mwNormal) {
|
|
31457
|
+
let density = 0.3;
|
|
31458
|
+
if (opts.milkyWayEnabled) {
|
|
31459
|
+
const dot = v.dot(mwNormal);
|
|
31460
|
+
const dist = Math.abs(dot);
|
|
31461
|
+
const band = Math.exp(-(dist * dist) / (opts.milkyWayWidth * opts.milkyWayWidth));
|
|
31462
|
+
density += band * opts.milkyWayStrength;
|
|
31463
|
+
}
|
|
31464
|
+
const noise = simpleNoise3D(v, opts.noiseScale);
|
|
31465
|
+
density *= 1 + noise * opts.noiseStrength;
|
|
31466
|
+
return Math.max(0.01, density);
|
|
31467
|
+
}
|
|
31468
|
+
function generateArrangement(bible, options = {}) {
|
|
31469
|
+
const opts = { ...defaultGenerateOptions, ...options };
|
|
31470
|
+
const rng = new RNG(opts.seed);
|
|
31471
|
+
const arrangement = {};
|
|
31472
|
+
const books = [];
|
|
31473
|
+
bible.testaments.forEach((t) => {
|
|
31474
|
+
t.divisions.forEach((d) => {
|
|
31475
|
+
d.books.forEach((b) => {
|
|
31476
|
+
books.push({
|
|
31477
|
+
key: b.key,
|
|
31478
|
+
name: b.name,
|
|
31479
|
+
chapters: b.chapters,
|
|
31480
|
+
division: d.name,
|
|
31481
|
+
testament: t.name
|
|
31482
|
+
});
|
|
31483
|
+
});
|
|
31484
|
+
});
|
|
31485
|
+
});
|
|
31486
|
+
const bookCount = books.length;
|
|
31487
|
+
const mwRad = THREE5.MathUtils.degToRad(opts.milkyWayAngle);
|
|
31488
|
+
const mwNormal = new THREE5.Vector3(Math.sin(mwRad), Math.cos(mwRad), 0).normalize();
|
|
31489
|
+
const anchors = [];
|
|
31490
|
+
for (let i = 0; i < bookCount; i++) {
|
|
31491
|
+
let bestP = new THREE5.Vector3();
|
|
31492
|
+
let valid = false;
|
|
31493
|
+
let attempt = 0;
|
|
31494
|
+
while (!valid && attempt < 100) {
|
|
31495
|
+
const p = rng.randomOnSphere();
|
|
31496
|
+
const d = getDensity(p, opts, mwNormal);
|
|
31497
|
+
if (rng.next() < d) {
|
|
31498
|
+
bestP = p;
|
|
31499
|
+
valid = true;
|
|
31500
|
+
}
|
|
31501
|
+
attempt++;
|
|
31502
|
+
}
|
|
31503
|
+
if (!valid) bestP = rng.randomOnSphere();
|
|
31504
|
+
anchors.push(bestP);
|
|
31505
|
+
}
|
|
31506
|
+
anchors.sort((a, b) => {
|
|
31507
|
+
const lonA = Math.atan2(a.z, a.x);
|
|
31508
|
+
const lonB = Math.atan2(b.z, b.x);
|
|
31509
|
+
return lonA - lonB;
|
|
31510
|
+
});
|
|
31511
|
+
books.forEach((book, i) => {
|
|
31512
|
+
const anchor = anchors[i];
|
|
31513
|
+
const anchorPos = anchor.clone().multiplyScalar(opts.discRadius);
|
|
31514
|
+
arrangement[`B:${book.key}`] = { position: [anchorPos.x, anchorPos.y, anchorPos.z] };
|
|
31515
|
+
for (let c = 0; c < book.chapters; c++) {
|
|
31516
|
+
const localSpread = opts.clusterSpread * (0.8 + rng.next() * 0.4);
|
|
31517
|
+
const offset = new THREE5.Vector3(
|
|
31518
|
+
(rng.next() - 0.5) * 2,
|
|
31519
|
+
(rng.next() - 0.5) * 2,
|
|
31520
|
+
(rng.next() - 0.5) * 2
|
|
31521
|
+
).normalize().multiplyScalar(rng.next() * localSpread);
|
|
31522
|
+
const starDir = anchor.clone().add(offset).normalize();
|
|
31523
|
+
if (starDir.y < 0.01) {
|
|
31524
|
+
starDir.y = 0.01;
|
|
31525
|
+
starDir.normalize();
|
|
31526
|
+
}
|
|
31527
|
+
const starPos = starDir.multiplyScalar(opts.discRadius);
|
|
31528
|
+
const chapId = `C:${book.key}:${c + 1}`;
|
|
31529
|
+
arrangement[chapId] = { position: [starPos.x, starPos.y, starPos.z] };
|
|
31530
|
+
}
|
|
31531
|
+
});
|
|
31532
|
+
const divisions = /* @__PURE__ */ new Map();
|
|
31533
|
+
books.forEach((book, i) => {
|
|
31534
|
+
const anchor = anchors[i];
|
|
31535
|
+
const anchorPos = anchor.clone().multiplyScalar(opts.discRadius);
|
|
31536
|
+
const divId = `D:${book.testament}:${book.division}`;
|
|
31537
|
+
if (!divisions.has(divId)) {
|
|
31538
|
+
divisions.set(divId, { sum: new THREE5.Vector3(), count: 0 });
|
|
31539
|
+
}
|
|
31540
|
+
const entry = divisions.get(divId);
|
|
31541
|
+
entry.sum.add(anchorPos);
|
|
31542
|
+
entry.count++;
|
|
31543
|
+
});
|
|
31544
|
+
divisions.forEach((val, key) => {
|
|
31545
|
+
if (val.count > 0) {
|
|
31546
|
+
val.sum.divideScalar(val.count);
|
|
31547
|
+
val.sum.normalize().multiplyScalar(opts.discRadius * 0.9);
|
|
31548
|
+
arrangement[key] = { position: [val.sum.x, val.sum.y, val.sum.z] };
|
|
31549
|
+
}
|
|
31550
|
+
});
|
|
31551
|
+
return arrangement;
|
|
31552
|
+
}
|
|
30471
31553
|
|
|
30472
|
-
export { StarMap, bibleToSceneModel, default_stars_default as defaultStars };
|
|
31554
|
+
export { StarMap, bibleToSceneModel, defaultGenerateOptions, default_stars_default as defaultStars, generateArrangement };
|
|
30473
31555
|
//# sourceMappingURL=index.js.map
|
|
30474
31556
|
//# sourceMappingURL=index.js.map
|