@project-skymap/library 0.4.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 +961 -289
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -1
- package/dist/index.d.ts +43 -1
- package/dist/index.js +960 -288
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var THREE5 = require('three');
|
|
4
4
|
var react = require('react');
|
|
5
5
|
var jsxRuntime = require('react/jsx-runtime');
|
|
6
6
|
|
|
@@ -22,7 +22,7 @@ function _interopNamespace(e) {
|
|
|
22
22
|
return Object.freeze(n);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
var
|
|
25
|
+
var THREE5__namespace = /*#__PURE__*/_interopNamespace(THREE5);
|
|
26
26
|
|
|
27
27
|
var __defProp = Object.defineProperty;
|
|
28
28
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -151,14 +151,14 @@ var init_constellations = __esm({
|
|
|
151
151
|
});
|
|
152
152
|
function lookAt(point, target, up) {
|
|
153
153
|
const zAxis = target.clone().normalize();
|
|
154
|
-
let xAxis = new
|
|
154
|
+
let xAxis = new THREE5__namespace.Vector3().crossVectors(up, zAxis);
|
|
155
155
|
if (xAxis.lengthSq() < 1e-4) {
|
|
156
|
-
xAxis = new
|
|
156
|
+
xAxis = new THREE5__namespace.Vector3().crossVectors(new THREE5__namespace.Vector3(1, 0, 0), zAxis);
|
|
157
157
|
}
|
|
158
158
|
xAxis.normalize();
|
|
159
|
-
const yAxis = new
|
|
160
|
-
const m = new
|
|
161
|
-
const v = new
|
|
159
|
+
const yAxis = new THREE5__namespace.Vector3().crossVectors(zAxis, xAxis).normalize();
|
|
160
|
+
const m = new THREE5__namespace.Matrix4().makeBasis(xAxis, yAxis, zAxis);
|
|
161
|
+
const v = new THREE5__namespace.Vector3(point.x, point.y, point.z);
|
|
162
162
|
v.applyMatrix4(m);
|
|
163
163
|
v.add(target);
|
|
164
164
|
return { x: v.x, y: v.y, z: v.z };
|
|
@@ -239,7 +239,7 @@ function computeLayoutPositions(model, layout) {
|
|
|
239
239
|
const radiusAtY = Math.sqrt(1 - y * y);
|
|
240
240
|
const x = Math.cos(midAngle) * radiusAtY;
|
|
241
241
|
const z = Math.sin(midAngle) * radiusAtY;
|
|
242
|
-
const labelPos = new
|
|
242
|
+
const labelPos = new THREE5__namespace.Vector3(x, y, z).multiplyScalar(radius);
|
|
243
243
|
uDivision.meta.x = labelPos.x;
|
|
244
244
|
uDivision.meta.y = labelPos.y;
|
|
245
245
|
uDivision.meta.z = labelPos.z;
|
|
@@ -255,7 +255,7 @@ function computeLayoutPositions(model, layout) {
|
|
|
255
255
|
const theta = startAngle + t * angleSpan;
|
|
256
256
|
const x = Math.cos(theta) * radiusAtY;
|
|
257
257
|
const z = Math.sin(theta) * radiusAtY;
|
|
258
|
-
const bookPos = new
|
|
258
|
+
const bookPos = new THREE5__namespace.Vector3(x, y, z).multiplyScalar(radius);
|
|
259
259
|
const labelPos = bookPos.clone();
|
|
260
260
|
labelPos.y += radius * 0.025;
|
|
261
261
|
labelPos.setLength(radius);
|
|
@@ -266,7 +266,7 @@ function computeLayoutPositions(model, layout) {
|
|
|
266
266
|
if (chapters.length > 0) {
|
|
267
267
|
const territoryRadius = radius * 2 / Math.sqrt(books.length * 2) * 0.7;
|
|
268
268
|
const localPoints = getConstellationLayout(bookKey, chapters.length, territoryRadius);
|
|
269
|
-
const up = new
|
|
269
|
+
const up = new THREE5__namespace.Vector3(0, 1, 0);
|
|
270
270
|
chapters.forEach((chap, idx) => {
|
|
271
271
|
const uChap = updatedNodeMap.get(chap.id);
|
|
272
272
|
const lp = localPoints[idx];
|
|
@@ -285,10 +285,10 @@ function computeLayoutPositions(model, layout) {
|
|
|
285
285
|
testaments.forEach((t) => {
|
|
286
286
|
const children = childrenMap.get(t.id) ?? [];
|
|
287
287
|
if (children.length === 0) return;
|
|
288
|
-
const centroid = new
|
|
288
|
+
const centroid = new THREE5__namespace.Vector3();
|
|
289
289
|
children.forEach((c) => {
|
|
290
290
|
const u = updatedNodeMap.get(c.id);
|
|
291
|
-
centroid.add(new
|
|
291
|
+
centroid.add(new THREE5__namespace.Vector3(u.meta.x, u.meta.y, u.meta.z));
|
|
292
292
|
});
|
|
293
293
|
centroid.divideScalar(children.length);
|
|
294
294
|
if (centroid.length() > 0.1) {
|
|
@@ -352,11 +352,18 @@ vec4 smartProject(vec4 viewPos) {
|
|
|
352
352
|
vec2 projected = vec2(k * dir.x, k * dir.y);
|
|
353
353
|
projected *= uScale;
|
|
354
354
|
projected.x /= uAspect;
|
|
355
|
-
float zMetric = -1.0 + (dist /
|
|
355
|
+
float zMetric = -1.0 + (dist / 15000.0);
|
|
356
|
+
|
|
357
|
+
// Radial Clipping: Push clipped points off-screen in their natural direction
|
|
358
|
+
// to prevent lines "darting" across the center.
|
|
359
|
+
vec2 escapeDir = (length(dir.xy) > 0.0001) ? normalize(dir.xy) : vec2(1.0, 1.0);
|
|
360
|
+
vec2 escapePos = escapeDir * 10000.0;
|
|
361
|
+
|
|
356
362
|
// Clip backward facing points in fisheye mode
|
|
357
|
-
if (uBlend > 0.5 && dir.z > 0.4) return vec4(
|
|
363
|
+
if (uBlend > 0.5 && dir.z > 0.4) return vec4(escapePos, 10.0, 1.0);
|
|
358
364
|
// Clip very close points in linear mode
|
|
359
|
-
if (uBlend < 0.1 && dir.z > -0.1) return vec4(
|
|
365
|
+
if (uBlend < 0.1 && dir.z > -0.1) return vec4(escapePos, 10.0, 1.0);
|
|
366
|
+
|
|
360
367
|
return vec4(projected, zMetric, 1.0);
|
|
361
368
|
}
|
|
362
369
|
`;
|
|
@@ -379,7 +386,7 @@ float getMaskAlpha() {
|
|
|
379
386
|
});
|
|
380
387
|
function createSmartMaterial(params) {
|
|
381
388
|
const uniforms = { ...globalUniforms, ...params.uniforms };
|
|
382
|
-
return new
|
|
389
|
+
return new THREE5__namespace.ShaderMaterial({
|
|
383
390
|
uniforms,
|
|
384
391
|
vertexShader: `
|
|
385
392
|
${BLEND_CHUNK}
|
|
@@ -393,8 +400,8 @@ function createSmartMaterial(params) {
|
|
|
393
400
|
transparent: params.transparent || false,
|
|
394
401
|
depthWrite: params.depthWrite !== void 0 ? params.depthWrite : true,
|
|
395
402
|
depthTest: params.depthTest !== void 0 ? params.depthTest : true,
|
|
396
|
-
side: params.side ||
|
|
397
|
-
blending: params.blending ||
|
|
403
|
+
side: params.side || THREE5__namespace.FrontSide,
|
|
404
|
+
blending: params.blending || THREE5__namespace.NormalBlending
|
|
398
405
|
});
|
|
399
406
|
}
|
|
400
407
|
var globalUniforms;
|
|
@@ -404,7 +411,248 @@ var init_materials = __esm({
|
|
|
404
411
|
globalUniforms = {
|
|
405
412
|
uScale: { value: 1 },
|
|
406
413
|
uAspect: { value: 1 },
|
|
407
|
-
uBlend: { value: 0 }
|
|
414
|
+
uBlend: { value: 0 },
|
|
415
|
+
uTime: { value: 0 },
|
|
416
|
+
// Atmosphere Settings
|
|
417
|
+
uAtmGlow: { value: 1 },
|
|
418
|
+
uAtmDark: { value: 0.6 },
|
|
419
|
+
uAtmExtinction: { value: 4 },
|
|
420
|
+
uAtmTwinkle: { value: 0.8 },
|
|
421
|
+
uColorHorizon: { value: new THREE5__namespace.Color(2768476) },
|
|
422
|
+
uColorZenith: { value: new THREE5__namespace.Color(132104) }
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
var ConstellationArtworkLayer;
|
|
427
|
+
var init_ConstellationArtworkLayer = __esm({
|
|
428
|
+
"src/engine/ConstellationArtworkLayer.ts"() {
|
|
429
|
+
init_materials();
|
|
430
|
+
ConstellationArtworkLayer = class {
|
|
431
|
+
root;
|
|
432
|
+
items = [];
|
|
433
|
+
textureLoader = new THREE5__namespace.TextureLoader();
|
|
434
|
+
hoveredId = null;
|
|
435
|
+
focusedId = null;
|
|
436
|
+
constructor(root) {
|
|
437
|
+
this.root = new THREE5__namespace.Group();
|
|
438
|
+
this.root.renderOrder = -1;
|
|
439
|
+
root.add(this.root);
|
|
440
|
+
}
|
|
441
|
+
getItems() {
|
|
442
|
+
return this.items;
|
|
443
|
+
}
|
|
444
|
+
setPosition(id, pos) {
|
|
445
|
+
const item = this.items.find((i) => i.config.id === id);
|
|
446
|
+
if (item) {
|
|
447
|
+
item.mesh.position.copy(pos);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
load(config, getPosition) {
|
|
451
|
+
this.clear();
|
|
452
|
+
const basePath = config.atlasBasePath.replace(/\/$/, "");
|
|
453
|
+
config.constellations.forEach((c) => {
|
|
454
|
+
let center = new THREE5__namespace.Vector3();
|
|
455
|
+
let valid = false;
|
|
456
|
+
let radius = 2e3;
|
|
457
|
+
const arrPos = getPosition(c.id);
|
|
458
|
+
if (arrPos) {
|
|
459
|
+
center.copy(arrPos);
|
|
460
|
+
valid = true;
|
|
461
|
+
if (c.anchors.length > 0) {
|
|
462
|
+
const points = [];
|
|
463
|
+
for (const anchorId of c.anchors) {
|
|
464
|
+
const p = getPosition(anchorId);
|
|
465
|
+
if (p) points.push(p);
|
|
466
|
+
}
|
|
467
|
+
if (points.length > 0) {
|
|
468
|
+
radius = points[0].length();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
} else if (c.center) {
|
|
472
|
+
center.set(c.center[0], c.center[1], c.center[2]);
|
|
473
|
+
valid = true;
|
|
474
|
+
} else if (c.anchors.length > 0) {
|
|
475
|
+
const points = [];
|
|
476
|
+
for (const anchorId of c.anchors) {
|
|
477
|
+
const p = getPosition(anchorId);
|
|
478
|
+
if (p) points.push(p);
|
|
479
|
+
}
|
|
480
|
+
if (points.length > 0) {
|
|
481
|
+
for (const p of points) center.add(p);
|
|
482
|
+
center.divideScalar(points.length);
|
|
483
|
+
const len = center.length();
|
|
484
|
+
if (len > 1e-3) {
|
|
485
|
+
radius = points[0].length();
|
|
486
|
+
center.normalize().multiplyScalar(radius);
|
|
487
|
+
}
|
|
488
|
+
valid = true;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (!valid) return;
|
|
492
|
+
const normal = center.clone().normalize().negate();
|
|
493
|
+
const upVec = center.clone().normalize();
|
|
494
|
+
let right = new THREE5__namespace.Vector3(1, 0, 0);
|
|
495
|
+
if (c.anchors.length >= 2) {
|
|
496
|
+
const p0 = getPosition(c.anchors[0]);
|
|
497
|
+
const p1 = getPosition(c.anchors[1]);
|
|
498
|
+
if (p0 && p1) {
|
|
499
|
+
const diff = new THREE5__namespace.Vector3().subVectors(p1, p0);
|
|
500
|
+
right.copy(diff).sub(upVec.clone().multiplyScalar(diff.dot(upVec))).normalize();
|
|
501
|
+
}
|
|
502
|
+
} else {
|
|
503
|
+
if (Math.abs(upVec.y) > 0.9) right.set(1, 0, 0).cross(upVec).normalize();
|
|
504
|
+
else right.set(0, 1, 0).cross(upVec).normalize();
|
|
505
|
+
}
|
|
506
|
+
const top = new THREE5__namespace.Vector3().crossVectors(upVec, right).normalize();
|
|
507
|
+
right.crossVectors(top, upVec).normalize();
|
|
508
|
+
new THREE5__namespace.Matrix4().makeBasis(right, top, normal);
|
|
509
|
+
const geometry = new THREE5__namespace.PlaneGeometry(1, 1);
|
|
510
|
+
let size = c.radius;
|
|
511
|
+
if (size <= 1) size *= radius;
|
|
512
|
+
size *= 2;
|
|
513
|
+
const texPath = `${basePath}/${c.image}`;
|
|
514
|
+
let blending = THREE5__namespace.NormalBlending;
|
|
515
|
+
if (c.blend === "additive") blending = THREE5__namespace.AdditiveBlending;
|
|
516
|
+
const material = createSmartMaterial({
|
|
517
|
+
uniforms: {
|
|
518
|
+
uMap: { value: this.textureLoader.load(texPath) },
|
|
519
|
+
// Placeholder, updated below
|
|
520
|
+
uOpacity: { value: c.opacity },
|
|
521
|
+
uSize: { value: size },
|
|
522
|
+
uImgRotation: { value: THREE5__namespace.MathUtils.degToRad(c.rotationDeg) },
|
|
523
|
+
uImgAspect: { value: c.aspectRatio ?? 1 }
|
|
524
|
+
// uScale, uAspect (screen) are injected by createSmartMaterial/globalUniforms
|
|
525
|
+
},
|
|
526
|
+
vertexShaderBody: `
|
|
527
|
+
uniform float uSize;
|
|
528
|
+
uniform float uImgRotation;
|
|
529
|
+
uniform float uImgAspect;
|
|
530
|
+
|
|
531
|
+
varying vec2 vUv;
|
|
532
|
+
|
|
533
|
+
void main() {
|
|
534
|
+
vUv = uv;
|
|
535
|
+
|
|
536
|
+
// 1. Project Center Point (Proven Method)
|
|
537
|
+
vec4 mvCenter = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
538
|
+
vec4 clipCenter = smartProject(mvCenter);
|
|
539
|
+
|
|
540
|
+
// 2. Project "Up" Point (World Zenith)
|
|
541
|
+
// Transform World Up (0,1,0) to View Space
|
|
542
|
+
vec3 viewUpDir = mat3(viewMatrix) * vec3(0.0, 1.0, 0.0);
|
|
543
|
+
// Offset center by a significant amount (1000.0) to ensure screen delta
|
|
544
|
+
vec4 mvUp = mvCenter + vec4(viewUpDir * 1000.0, 0.0);
|
|
545
|
+
vec4 clipUp = smartProject(mvUp);
|
|
546
|
+
|
|
547
|
+
// 3. Calculate Horizon Angle
|
|
548
|
+
vec2 screenCenter = clipCenter.xy / clipCenter.w;
|
|
549
|
+
vec2 screenUp = clipUp.xy / clipUp.w;
|
|
550
|
+
vec2 screenDelta = screenUp - screenCenter;
|
|
551
|
+
|
|
552
|
+
float horizonAngle = 0.0;
|
|
553
|
+
if (length(screenDelta) > 0.001) {
|
|
554
|
+
vec2 screenDir = normalize(screenDelta);
|
|
555
|
+
horizonAngle = atan(screenDir.y, screenDir.x) - 1.5708; // -90 deg
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// 4. Combine with User Rotation
|
|
559
|
+
float finalAngle = uImgRotation + horizonAngle;
|
|
560
|
+
|
|
561
|
+
// 5. Billboard Offset
|
|
562
|
+
vec2 offset = position.xy;
|
|
563
|
+
|
|
564
|
+
float cr = cos(finalAngle);
|
|
565
|
+
float sr = sin(finalAngle);
|
|
566
|
+
vec2 rotated = vec2(
|
|
567
|
+
offset.x * cr - offset.y * sr,
|
|
568
|
+
offset.x * sr + offset.y * cr
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
rotated.x *= uImgAspect;
|
|
572
|
+
|
|
573
|
+
float dist = length(mvCenter.xyz);
|
|
574
|
+
float scale = (uSize / dist) * uScale;
|
|
575
|
+
|
|
576
|
+
rotated *= scale;
|
|
577
|
+
rotated.x /= uAspect;
|
|
578
|
+
|
|
579
|
+
gl_Position = clipCenter;
|
|
580
|
+
gl_Position.xy += rotated * clipCenter.w;
|
|
581
|
+
|
|
582
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
583
|
+
}
|
|
584
|
+
`,
|
|
585
|
+
fragmentShader: `
|
|
586
|
+
uniform sampler2D uMap;
|
|
587
|
+
uniform float uOpacity;
|
|
588
|
+
varying vec2 vUv;
|
|
589
|
+
void main() {
|
|
590
|
+
float mask = getMaskAlpha();
|
|
591
|
+
if (mask < 0.01) discard;
|
|
592
|
+
vec4 tex = texture2D(uMap, vUv);
|
|
593
|
+
gl_FragColor = vec4(tex.rgb, tex.a * uOpacity * mask);
|
|
594
|
+
}
|
|
595
|
+
`,
|
|
596
|
+
transparent: true,
|
|
597
|
+
depthWrite: false,
|
|
598
|
+
depthTest: true,
|
|
599
|
+
blending,
|
|
600
|
+
side: THREE5__namespace.DoubleSide
|
|
601
|
+
});
|
|
602
|
+
material.uniforms.uMap.value = this.textureLoader.load(texPath, (tex) => {
|
|
603
|
+
if (c.aspectRatio === void 0 && tex.image.width && tex.image.height) {
|
|
604
|
+
const natAspect = tex.image.width / tex.image.height;
|
|
605
|
+
material.uniforms.uImgAspect.value = natAspect;
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
if (c.zBias) {
|
|
609
|
+
material.polygonOffset = true;
|
|
610
|
+
material.polygonOffsetFactor = -c.zBias;
|
|
611
|
+
}
|
|
612
|
+
const mesh = new THREE5__namespace.Mesh(geometry, material);
|
|
613
|
+
mesh.frustumCulled = false;
|
|
614
|
+
mesh.userData = { id: c.id, type: "constellation" };
|
|
615
|
+
mesh.position.copy(center);
|
|
616
|
+
this.root.add(mesh);
|
|
617
|
+
this.items.push({ config: c, mesh, material, baseOpacity: c.opacity });
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
update(fov, showArt) {
|
|
621
|
+
this.root.visible = showArt;
|
|
622
|
+
if (!showArt) return;
|
|
623
|
+
for (const item of this.items) {
|
|
624
|
+
const { fade } = item.config;
|
|
625
|
+
let opacity = fade.maxOpacity;
|
|
626
|
+
if (fov >= fade.zoomInStart) {
|
|
627
|
+
opacity = fade.maxOpacity;
|
|
628
|
+
} else if (fov <= fade.zoomInEnd) {
|
|
629
|
+
opacity = fade.minOpacity;
|
|
630
|
+
} else {
|
|
631
|
+
const t = (fade.zoomInStart - fov) / (fade.zoomInStart - fade.zoomInEnd);
|
|
632
|
+
opacity = THREE5__namespace.MathUtils.lerp(fade.maxOpacity, fade.minOpacity, t);
|
|
633
|
+
}
|
|
634
|
+
opacity = Math.min(Math.max(opacity, 0), 1);
|
|
635
|
+
item.material.uniforms.uOpacity.value = opacity;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
setHovered(id) {
|
|
639
|
+
this.hoveredId = id;
|
|
640
|
+
}
|
|
641
|
+
setFocused(id) {
|
|
642
|
+
this.focusedId = id;
|
|
643
|
+
}
|
|
644
|
+
dispose() {
|
|
645
|
+
this.clear();
|
|
646
|
+
this.root.removeFromParent();
|
|
647
|
+
}
|
|
648
|
+
clear() {
|
|
649
|
+
this.items.forEach((i) => {
|
|
650
|
+
this.root.remove(i.mesh);
|
|
651
|
+
i.material.dispose();
|
|
652
|
+
i.mesh.geometry.dispose();
|
|
653
|
+
});
|
|
654
|
+
this.items = [];
|
|
655
|
+
}
|
|
408
656
|
};
|
|
409
657
|
}
|
|
410
658
|
});
|
|
@@ -418,17 +666,37 @@ function createEngine({
|
|
|
418
666
|
container,
|
|
419
667
|
onSelect,
|
|
420
668
|
onHover,
|
|
421
|
-
onArrangementChange
|
|
669
|
+
onArrangementChange,
|
|
670
|
+
onFovChange
|
|
422
671
|
}) {
|
|
423
|
-
|
|
672
|
+
let hoveredBookId = null;
|
|
673
|
+
let focusedBookId = null;
|
|
674
|
+
let orderRevealEnabled = true;
|
|
675
|
+
let activeBookIndex = -1;
|
|
676
|
+
let orderRevealStrength = 0;
|
|
677
|
+
const hoverCooldowns = /* @__PURE__ */ new Map();
|
|
678
|
+
const COOLDOWN_MS = 2e3;
|
|
679
|
+
const bookIdToIndex = /* @__PURE__ */ new Map();
|
|
680
|
+
const renderer = new THREE5__namespace.WebGLRenderer({ antialias: true, alpha: false });
|
|
424
681
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
425
682
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
426
683
|
container.appendChild(renderer.domElement);
|
|
427
|
-
const scene = new
|
|
428
|
-
scene.background = new
|
|
429
|
-
const camera = new
|
|
684
|
+
const scene = new THREE5__namespace.Scene();
|
|
685
|
+
scene.background = new THREE5__namespace.Color(0);
|
|
686
|
+
const camera = new THREE5__namespace.PerspectiveCamera(60, 1, 0.1, 1e4);
|
|
430
687
|
camera.position.set(0, 0, 0);
|
|
431
688
|
camera.up.set(0, 1, 0);
|
|
689
|
+
function setHoveredBook(id) {
|
|
690
|
+
if (id === hoveredBookId) return;
|
|
691
|
+
const now = performance.now();
|
|
692
|
+
if (hoveredBookId) {
|
|
693
|
+
hoverCooldowns.set(hoveredBookId, now);
|
|
694
|
+
}
|
|
695
|
+
if (id) {
|
|
696
|
+
hoverCooldowns.get(id) || 0;
|
|
697
|
+
}
|
|
698
|
+
hoveredBookId = id;
|
|
699
|
+
}
|
|
432
700
|
let running = false;
|
|
433
701
|
let raf = 0;
|
|
434
702
|
const state = {
|
|
@@ -449,10 +717,12 @@ function createEngine({
|
|
|
449
717
|
draggedGroup: null,
|
|
450
718
|
tempArrangement: {}
|
|
451
719
|
};
|
|
452
|
-
const mouseNDC = new
|
|
720
|
+
const mouseNDC = new THREE5__namespace.Vector2();
|
|
453
721
|
let isMouseInWindow = false;
|
|
454
|
-
let
|
|
722
|
+
let edgeHoverStart = 0;
|
|
723
|
+
let handlers = { onSelect, onHover, onArrangementChange, onFovChange };
|
|
455
724
|
let currentConfig;
|
|
725
|
+
const constellationLayer = new ConstellationArtworkLayer(scene);
|
|
456
726
|
function mix(a, b, t) {
|
|
457
727
|
return a * (1 - t) + b * t;
|
|
458
728
|
}
|
|
@@ -487,7 +757,7 @@ function createEngine({
|
|
|
487
757
|
const phi = Math.atan2(uvY, uvX);
|
|
488
758
|
const sinTheta = Math.sin(theta);
|
|
489
759
|
const cosTheta = Math.cos(theta);
|
|
490
|
-
return new
|
|
760
|
+
return new THREE5__namespace.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
|
|
491
761
|
}
|
|
492
762
|
function getMouseWorldVector(pixelX, pixelY, width, height) {
|
|
493
763
|
const aspect = width / height;
|
|
@@ -506,7 +776,7 @@ function createEngine({
|
|
|
506
776
|
const phi = Math.atan2(uvY, uvX);
|
|
507
777
|
const sinTheta = Math.sin(theta);
|
|
508
778
|
const cosTheta = Math.cos(theta);
|
|
509
|
-
const vView = new
|
|
779
|
+
const vView = new THREE5__namespace.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
|
|
510
780
|
return vView.applyQuaternion(camera.quaternion);
|
|
511
781
|
}
|
|
512
782
|
function smartProjectJS(worldPos) {
|
|
@@ -519,147 +789,187 @@ function createEngine({
|
|
|
519
789
|
const k = mix(kLinear, kStereo, blend);
|
|
520
790
|
return { x: k * dir.x, y: k * dir.y, z: dir.z };
|
|
521
791
|
}
|
|
522
|
-
const groundGroup = new
|
|
792
|
+
const groundGroup = new THREE5__namespace.Group();
|
|
523
793
|
scene.add(groundGroup);
|
|
524
794
|
function createGround() {
|
|
525
795
|
groundGroup.clear();
|
|
526
796
|
const radius = 995;
|
|
527
|
-
const geometry = new
|
|
797
|
+
const geometry = new THREE5__namespace.SphereGeometry(radius, 128, 64, 0, Math.PI * 2, Math.PI / 2 - 0.15, Math.PI / 2 + 0.15);
|
|
528
798
|
const material = createSmartMaterial({
|
|
529
|
-
uniforms: {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
799
|
+
uniforms: {
|
|
800
|
+
color: { value: new THREE5__namespace.Color(131587) },
|
|
801
|
+
// Very dark almost black
|
|
802
|
+
fogColor: { value: new THREE5__namespace.Color(331812) }
|
|
803
|
+
// Matches atmosphere bot color
|
|
804
|
+
},
|
|
805
|
+
vertexShaderBody: `
|
|
806
|
+
varying vec3 vPos;
|
|
807
|
+
varying vec3 vWorldPos;
|
|
808
|
+
void main() {
|
|
809
|
+
vPos = position;
|
|
810
|
+
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
811
|
+
gl_Position = smartProject(mvPosition);
|
|
812
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
813
|
+
vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
|
|
814
|
+
}
|
|
815
|
+
`,
|
|
816
|
+
fragmentShader: `
|
|
817
|
+
uniform vec3 color;
|
|
818
|
+
uniform vec3 fogColor;
|
|
819
|
+
varying vec3 vPos;
|
|
820
|
+
varying vec3 vWorldPos;
|
|
821
|
+
|
|
822
|
+
void main() {
|
|
823
|
+
float alphaMask = getMaskAlpha();
|
|
824
|
+
if (alphaMask < 0.01) discard;
|
|
825
|
+
|
|
826
|
+
// Procedural Horizon (Mountains)
|
|
827
|
+
float angle = atan(vPos.z, vPos.x);
|
|
828
|
+
|
|
829
|
+
// Simple FBM-like terrain
|
|
830
|
+
float h = 0.0;
|
|
831
|
+
h += sin(angle * 6.0) * 20.0;
|
|
832
|
+
h += sin(angle * 13.0 + 1.0) * 10.0;
|
|
833
|
+
h += sin(angle * 29.0 + 2.0) * 5.0;
|
|
834
|
+
h += sin(angle * 63.0 + 4.0) * 2.0;
|
|
835
|
+
|
|
836
|
+
// Base horizon offset (lift slightly)
|
|
837
|
+
float terrainHeight = h + 10.0;
|
|
838
|
+
|
|
839
|
+
if (vPos.y > terrainHeight) discard;
|
|
840
|
+
|
|
841
|
+
// Atmospheric Haze / Fog on the ground
|
|
842
|
+
// Mix ground color with fog color based on vertical height (fade into horizon)
|
|
843
|
+
// Closer to horizon (higher y) -> more fog
|
|
844
|
+
float fogFactor = smoothstep(-100.0, terrainHeight, vPos.y);
|
|
845
|
+
vec3 finalCol = mix(color, fogColor, fogFactor * 0.5);
|
|
846
|
+
|
|
847
|
+
gl_FragColor = vec4(finalCol, 1.0);
|
|
848
|
+
}
|
|
849
|
+
`,
|
|
850
|
+
side: THREE5__namespace.BackSide,
|
|
533
851
|
transparent: false,
|
|
534
852
|
depthWrite: true,
|
|
535
853
|
depthTest: true
|
|
536
854
|
});
|
|
537
|
-
const ground = new
|
|
855
|
+
const ground = new THREE5__namespace.Mesh(geometry, material);
|
|
538
856
|
groundGroup.add(ground);
|
|
539
|
-
const boxGeo = new THREE4__namespace.BoxGeometry(8, 30, 8);
|
|
540
|
-
for (let i = 0; i < 12; i++) {
|
|
541
|
-
const angle = i / 12 * Math.PI * 2;
|
|
542
|
-
const b = new THREE4__namespace.Mesh(boxGeo, material);
|
|
543
|
-
const r = radius * 0.98;
|
|
544
|
-
b.position.set(Math.cos(angle) * r, -15, Math.sin(angle) * r);
|
|
545
|
-
b.lookAt(0, 0, 0);
|
|
546
|
-
groundGroup.add(b);
|
|
547
|
-
}
|
|
548
857
|
}
|
|
858
|
+
let atmosphereMesh = null;
|
|
549
859
|
function createAtmosphere() {
|
|
550
|
-
const geometry = new
|
|
860
|
+
const geometry = new THREE5__namespace.SphereGeometry(990, 64, 64);
|
|
551
861
|
const material = createSmartMaterial({
|
|
552
|
-
uniforms: { top: { value: new THREE4__namespace.Color(0) }, bot: { value: new THREE4__namespace.Color(1712172) } },
|
|
553
862
|
vertexShaderBody: `
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
vec4 mv = modelViewMatrix * vec4(position, 1.0);
|
|
562
|
-
|
|
563
|
-
gl_Position = smartProject(mv);
|
|
564
|
-
|
|
565
|
-
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
`,
|
|
863
|
+
varying vec3 vWorldNormal;
|
|
864
|
+
void main() {
|
|
865
|
+
vWorldNormal = normalize(position);
|
|
866
|
+
vec4 mv = modelViewMatrix * vec4(position, 1.0);
|
|
867
|
+
gl_Position = smartProject(mv);
|
|
868
|
+
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
869
|
+
}`,
|
|
570
870
|
fragmentShader: `
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
871
|
+
varying vec3 vWorldNormal;
|
|
872
|
+
|
|
873
|
+
uniform float uAtmGlow;
|
|
874
|
+
uniform float uAtmDark;
|
|
875
|
+
uniform vec3 uColorHorizon;
|
|
876
|
+
uniform vec3 uColorZenith;
|
|
877
|
+
|
|
878
|
+
void main() {
|
|
879
|
+
float alphaMask = getMaskAlpha();
|
|
880
|
+
if (alphaMask < 0.01) discard;
|
|
881
|
+
|
|
882
|
+
// Altitude angle (Y is up)
|
|
883
|
+
float h = normalize(vWorldNormal).y;
|
|
884
|
+
|
|
885
|
+
// Gradient Logic
|
|
886
|
+
// 1. Base gradient from Horizon to Zenith
|
|
887
|
+
float t = smoothstep(-0.1, 0.5, h);
|
|
888
|
+
|
|
889
|
+
// Non-linear mix for realistic sky falloff
|
|
890
|
+
// Zenith darkness adjustment
|
|
891
|
+
vec3 skyColor = mix(uColorHorizon * uAtmGlow, uColorZenith * (1.0 - uAtmDark), pow(t, 0.6));
|
|
892
|
+
|
|
893
|
+
// 2. Horizon Glow Band (Simulate scattering/haze layer)
|
|
894
|
+
float horizonBand = exp(-15.0 * abs(h - 0.02)); // Sharp peak near 0
|
|
895
|
+
skyColor += uColorHorizon * horizonBand * 0.5 * uAtmGlow;
|
|
896
|
+
|
|
897
|
+
gl_FragColor = vec4(skyColor, 1.0);
|
|
898
|
+
}
|
|
899
|
+
`,
|
|
900
|
+
side: THREE5__namespace.BackSide,
|
|
594
901
|
depthWrite: false,
|
|
595
902
|
depthTest: true
|
|
596
903
|
});
|
|
597
|
-
const atm = new
|
|
904
|
+
const atm = new THREE5__namespace.Mesh(geometry, material);
|
|
905
|
+
atmosphereMesh = atm;
|
|
598
906
|
groundGroup.add(atm);
|
|
599
907
|
}
|
|
600
|
-
const backdropGroup = new
|
|
908
|
+
const backdropGroup = new THREE5__namespace.Group();
|
|
601
909
|
scene.add(backdropGroup);
|
|
602
|
-
function createBackdropStars() {
|
|
910
|
+
function createBackdropStars(count = 31e3) {
|
|
603
911
|
backdropGroup.clear();
|
|
604
|
-
|
|
912
|
+
while (backdropGroup.children.length > 0) {
|
|
913
|
+
const c = backdropGroup.children[0];
|
|
914
|
+
backdropGroup.remove(c);
|
|
915
|
+
if (c.geometry) c.geometry.dispose();
|
|
916
|
+
if (c.material) c.material.dispose();
|
|
917
|
+
}
|
|
918
|
+
const geometry = new THREE5__namespace.BufferGeometry();
|
|
605
919
|
const positions = [];
|
|
606
920
|
const sizes = [];
|
|
607
921
|
const colors = [];
|
|
608
|
-
const colorPalette = [
|
|
609
|
-
new THREE4__namespace.Color(10203391),
|
|
610
|
-
new THREE4__namespace.Color(11190271),
|
|
611
|
-
new THREE4__namespace.Color(13293567),
|
|
612
|
-
new THREE4__namespace.Color(16316415),
|
|
613
|
-
new THREE4__namespace.Color(16774378),
|
|
614
|
-
new THREE4__namespace.Color(16765601),
|
|
615
|
-
new THREE4__namespace.Color(16764015)
|
|
616
|
-
];
|
|
617
922
|
const r = 2500;
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
v.normalize();
|
|
627
|
-
v.applyAxisAngle(new THREE4__namespace.Vector3(1, 0, 0), THREE4__namespace.MathUtils.degToRad(60));
|
|
628
|
-
x = v.x * r;
|
|
629
|
-
y = v.y * r;
|
|
630
|
-
z = v.z * r;
|
|
631
|
-
} else {
|
|
632
|
-
const u = Math.random();
|
|
633
|
-
const v = Math.random();
|
|
634
|
-
const theta = 2 * Math.PI * u;
|
|
635
|
-
const phi = Math.acos(2 * v - 1);
|
|
636
|
-
x = r * Math.sin(phi) * Math.cos(theta);
|
|
637
|
-
y = r * Math.sin(phi) * Math.sin(theta);
|
|
638
|
-
z = r * Math.cos(phi);
|
|
639
|
-
}
|
|
923
|
+
for (let i = 0; i < count; i++) {
|
|
924
|
+
const u = Math.random();
|
|
925
|
+
const v = Math.random();
|
|
926
|
+
const theta = 2 * Math.PI * u;
|
|
927
|
+
const phi = Math.acos(2 * v - 1);
|
|
928
|
+
const x = r * Math.sin(phi) * Math.cos(theta);
|
|
929
|
+
const y = r * Math.cos(phi);
|
|
930
|
+
const z = r * Math.sin(phi) * Math.sin(theta);
|
|
640
931
|
positions.push(x, y, z);
|
|
641
|
-
const size =
|
|
932
|
+
const size = 1 + -Math.log(Math.random()) * 0.8 * 1.5;
|
|
642
933
|
sizes.push(size);
|
|
643
|
-
|
|
644
|
-
const c = colorPalette[cIndex];
|
|
645
|
-
colors.push(c.r, c.g, c.b);
|
|
934
|
+
colors.push(1, 1, 1);
|
|
646
935
|
}
|
|
647
|
-
geometry.setAttribute("position", new
|
|
648
|
-
geometry.setAttribute("size", new
|
|
649
|
-
geometry.setAttribute("color", new
|
|
936
|
+
geometry.setAttribute("position", new THREE5__namespace.Float32BufferAttribute(positions, 3));
|
|
937
|
+
geometry.setAttribute("size", new THREE5__namespace.Float32BufferAttribute(sizes, 1));
|
|
938
|
+
geometry.setAttribute("color", new THREE5__namespace.Float32BufferAttribute(colors, 3));
|
|
650
939
|
const material = createSmartMaterial({
|
|
651
|
-
uniforms: {
|
|
940
|
+
uniforms: {
|
|
941
|
+
pixelRatio: { value: renderer.getPixelRatio() },
|
|
942
|
+
uScale: globalUniforms.uScale
|
|
943
|
+
},
|
|
652
944
|
vertexShaderBody: `
|
|
653
945
|
attribute float size;
|
|
654
946
|
attribute vec3 color;
|
|
655
947
|
varying vec3 vColor;
|
|
656
948
|
uniform float pixelRatio;
|
|
949
|
+
|
|
950
|
+
uniform float uAtmExtinction;
|
|
951
|
+
|
|
657
952
|
void main() {
|
|
658
|
-
|
|
953
|
+
vec3 nPos = normalize(position);
|
|
954
|
+
float altitude = nPos.y;
|
|
955
|
+
|
|
956
|
+
// Simple Extinction & Horizon Fade
|
|
957
|
+
float horizonFade = smoothstep(-0.1, 0.1, altitude);
|
|
958
|
+
float airmass = 1.0 / (max(0.05, altitude + 0.05));
|
|
959
|
+
float extinction = exp(-uAtmExtinction * 0.15 * airmass);
|
|
960
|
+
|
|
961
|
+
// Boost intensity significantly (3.0x)
|
|
962
|
+
vColor = color * 3.0 * extinction * horizonFade;
|
|
963
|
+
|
|
659
964
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
660
965
|
gl_Position = smartProject(mvPosition);
|
|
661
966
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
662
|
-
|
|
967
|
+
|
|
968
|
+
// Non-linear scale with zoom to keep stars looking like points
|
|
969
|
+
// pow(uScale, 0.5) prevents them from getting too large at low FOV
|
|
970
|
+
float zoomScale = pow(uScale, 0.5);
|
|
971
|
+
|
|
972
|
+
gl_PointSize = size * zoomScale * 0.5 * pixelRatio * (800.0 / -mvPosition.z) * horizonFade;
|
|
663
973
|
}
|
|
664
974
|
`,
|
|
665
975
|
fragmentShader: `
|
|
@@ -670,26 +980,28 @@ function createEngine({
|
|
|
670
980
|
if (dist > 1.0) discard;
|
|
671
981
|
float alphaMask = getMaskAlpha();
|
|
672
982
|
if (alphaMask < 0.01) discard;
|
|
673
|
-
|
|
674
|
-
|
|
983
|
+
|
|
984
|
+
// Sharp falloff for intense point look
|
|
985
|
+
float alpha = exp(-4.0 * dist * dist);
|
|
675
986
|
gl_FragColor = vec4(vColor, alpha * alphaMask);
|
|
676
987
|
}
|
|
677
988
|
`,
|
|
678
989
|
transparent: true,
|
|
679
990
|
depthWrite: false,
|
|
680
|
-
depthTest: true
|
|
991
|
+
depthTest: true,
|
|
992
|
+
blending: THREE5__namespace.AdditiveBlending
|
|
681
993
|
});
|
|
682
|
-
const points = new
|
|
994
|
+
const points = new THREE5__namespace.Points(geometry, material);
|
|
683
995
|
points.frustumCulled = false;
|
|
684
996
|
backdropGroup.add(points);
|
|
685
997
|
}
|
|
686
998
|
createGround();
|
|
687
999
|
createAtmosphere();
|
|
688
1000
|
createBackdropStars();
|
|
689
|
-
const raycaster = new
|
|
1001
|
+
const raycaster = new THREE5__namespace.Raycaster();
|
|
690
1002
|
raycaster.params.Points.threshold = 5;
|
|
691
|
-
new
|
|
692
|
-
const root = new
|
|
1003
|
+
new THREE5__namespace.Vector2();
|
|
1004
|
+
const root = new THREE5__namespace.Group();
|
|
693
1005
|
scene.add(root);
|
|
694
1006
|
const nodeById = /* @__PURE__ */ new Map();
|
|
695
1007
|
const starIndexToId = [];
|
|
@@ -697,7 +1009,7 @@ function createEngine({
|
|
|
697
1009
|
const hoverLabelMat = createSmartMaterial({
|
|
698
1010
|
uniforms: {
|
|
699
1011
|
uMap: { value: null },
|
|
700
|
-
uSize: { value: new
|
|
1012
|
+
uSize: { value: new THREE5__namespace.Vector2(1, 1) },
|
|
701
1013
|
uAlpha: { value: 0 },
|
|
702
1014
|
uAngle: { value: 0 }
|
|
703
1015
|
},
|
|
@@ -735,7 +1047,7 @@ function createEngine({
|
|
|
735
1047
|
depthTest: false
|
|
736
1048
|
// Always on top of stars
|
|
737
1049
|
});
|
|
738
|
-
const hoverLabelMesh = new
|
|
1050
|
+
const hoverLabelMesh = new THREE5__namespace.Mesh(new THREE5__namespace.PlaneGeometry(1, 1), hoverLabelMat);
|
|
739
1051
|
hoverLabelMesh.visible = false;
|
|
740
1052
|
hoverLabelMesh.renderOrder = 999;
|
|
741
1053
|
hoverLabelMesh.frustumCulled = false;
|
|
@@ -766,19 +1078,20 @@ function createEngine({
|
|
|
766
1078
|
const ctx = canvas.getContext("2d");
|
|
767
1079
|
if (!ctx) return null;
|
|
768
1080
|
const fontSize = 96;
|
|
769
|
-
|
|
1081
|
+
const font = `400 ${fontSize}px "Inter", system-ui, sans-serif`;
|
|
1082
|
+
ctx.font = font;
|
|
770
1083
|
const metrics = ctx.measureText(text);
|
|
771
1084
|
const w = Math.ceil(metrics.width);
|
|
772
1085
|
const h = Math.ceil(fontSize * 1.2);
|
|
773
1086
|
canvas.width = w;
|
|
774
1087
|
canvas.height = h;
|
|
775
|
-
ctx.font =
|
|
1088
|
+
ctx.font = font;
|
|
776
1089
|
ctx.fillStyle = color;
|
|
777
1090
|
ctx.textAlign = "center";
|
|
778
1091
|
ctx.textBaseline = "middle";
|
|
779
1092
|
ctx.fillText(text, w / 2, h / 2);
|
|
780
|
-
const tex = new
|
|
781
|
-
tex.minFilter =
|
|
1093
|
+
const tex = new THREE5__namespace.CanvasTexture(canvas);
|
|
1094
|
+
tex.minFilter = THREE5__namespace.LinearFilter;
|
|
782
1095
|
return { tex, aspect: w / h };
|
|
783
1096
|
}
|
|
784
1097
|
function getPosition(n) {
|
|
@@ -792,27 +1105,28 @@ function createEngine({
|
|
|
792
1105
|
const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
|
|
793
1106
|
const phi = Math.atan2(y, x);
|
|
794
1107
|
const theta = r_norm * (Math.PI / 2);
|
|
795
|
-
return new
|
|
1108
|
+
return new THREE5__namespace.Vector3(
|
|
796
1109
|
Math.sin(theta) * Math.cos(phi),
|
|
797
1110
|
Math.cos(theta),
|
|
798
1111
|
Math.sin(theta) * Math.sin(phi)
|
|
799
1112
|
).multiplyScalar(radius);
|
|
800
1113
|
}
|
|
801
|
-
return new
|
|
1114
|
+
return new THREE5__namespace.Vector3(arr.position[0], arr.position[1], arr.position[2]);
|
|
802
1115
|
}
|
|
803
1116
|
}
|
|
804
|
-
return new
|
|
1117
|
+
return new THREE5__namespace.Vector3(n.meta?.x ?? 0, n.meta?.y ?? 0, n.meta?.z ?? 0);
|
|
805
1118
|
}
|
|
806
1119
|
function getBoundaryPoint(angle, t, radius) {
|
|
807
1120
|
const y = 0.05 + t * (1 - 0.05);
|
|
808
1121
|
const rY = Math.sqrt(1 - y * y);
|
|
809
1122
|
const x = Math.cos(angle) * rY;
|
|
810
1123
|
const z = Math.sin(angle) * rY;
|
|
811
|
-
return new
|
|
1124
|
+
return new THREE5__namespace.Vector3(x, y, z).multiplyScalar(radius);
|
|
812
1125
|
}
|
|
813
1126
|
function buildFromModel(model, cfg) {
|
|
814
1127
|
clearRoot();
|
|
815
|
-
|
|
1128
|
+
bookIdToIndex.clear();
|
|
1129
|
+
scene.background = cfg.background && cfg.background !== "transparent" ? new THREE5__namespace.Color(cfg.background) : new THREE5__namespace.Color(0);
|
|
816
1130
|
const layoutCfg = { ...cfg.layout, radius: cfg.layout?.radius ?? 2e3 };
|
|
817
1131
|
const laidOut = computeLayoutPositions(model, layoutCfg);
|
|
818
1132
|
const divisionPositions = /* @__PURE__ */ new Map();
|
|
@@ -826,7 +1140,7 @@ function createEngine({
|
|
|
826
1140
|
}
|
|
827
1141
|
}
|
|
828
1142
|
for (const [divId, books] of divMap.entries()) {
|
|
829
|
-
const centroid = new
|
|
1143
|
+
const centroid = new THREE5__namespace.Vector3();
|
|
830
1144
|
let count = 0;
|
|
831
1145
|
for (const b of books) {
|
|
832
1146
|
const p = getPosition(b);
|
|
@@ -842,21 +1156,24 @@ function createEngine({
|
|
|
842
1156
|
const starPositions = [];
|
|
843
1157
|
const starSizes = [];
|
|
844
1158
|
const starColors = [];
|
|
1159
|
+
const starPhases = [];
|
|
1160
|
+
const starBookIndices = [];
|
|
1161
|
+
const starChapterIndices = [];
|
|
845
1162
|
const SPECTRAL_COLORS = [
|
|
846
|
-
new
|
|
847
|
-
// O -
|
|
848
|
-
new
|
|
849
|
-
// B -
|
|
850
|
-
new
|
|
851
|
-
// A - White
|
|
852
|
-
new
|
|
1163
|
+
new THREE5__namespace.Color(14544639),
|
|
1164
|
+
// O - Blueish White
|
|
1165
|
+
new THREE5__namespace.Color(15660287),
|
|
1166
|
+
// B - White
|
|
1167
|
+
new THREE5__namespace.Color(16317695),
|
|
1168
|
+
// A - White
|
|
1169
|
+
new THREE5__namespace.Color(16777208),
|
|
853
1170
|
// F - White
|
|
854
|
-
new
|
|
855
|
-
// G -
|
|
856
|
-
new
|
|
857
|
-
// K -
|
|
858
|
-
new
|
|
859
|
-
// M - Orange
|
|
1171
|
+
new THREE5__namespace.Color(16775406),
|
|
1172
|
+
// G - Yellowish White
|
|
1173
|
+
new THREE5__namespace.Color(16773085),
|
|
1174
|
+
// K - Pale Orange
|
|
1175
|
+
new THREE5__namespace.Color(16771788)
|
|
1176
|
+
// M - Light Orange
|
|
860
1177
|
];
|
|
861
1178
|
let minWeight = Infinity;
|
|
862
1179
|
let maxWeight = -Infinity;
|
|
@@ -881,21 +1198,41 @@ function createEngine({
|
|
|
881
1198
|
let baseSize = 3.5;
|
|
882
1199
|
if (typeof n.weight === "number") {
|
|
883
1200
|
const t = (n.weight - minWeight) / (maxWeight - minWeight);
|
|
884
|
-
baseSize =
|
|
1201
|
+
baseSize = 0.1 + Math.pow(t, 0.5) * 11.9;
|
|
885
1202
|
}
|
|
886
1203
|
starSizes.push(baseSize);
|
|
887
1204
|
const colorIdx = Math.floor(Math.pow(Math.random(), 1.5) * SPECTRAL_COLORS.length);
|
|
888
1205
|
const c = SPECTRAL_COLORS[Math.min(colorIdx, SPECTRAL_COLORS.length - 1)];
|
|
889
1206
|
starColors.push(c.r, c.g, c.b);
|
|
1207
|
+
starPhases.push(Math.random() * Math.PI * 2);
|
|
1208
|
+
let bIdx = -1;
|
|
1209
|
+
if (n.parent) {
|
|
1210
|
+
if (!bookIdToIndex.has(n.parent)) {
|
|
1211
|
+
bookIdToIndex.set(n.parent, bookIdToIndex.size + 1);
|
|
1212
|
+
}
|
|
1213
|
+
bIdx = bookIdToIndex.get(n.parent);
|
|
1214
|
+
}
|
|
1215
|
+
starBookIndices.push(bIdx);
|
|
1216
|
+
let cIdx = 0;
|
|
1217
|
+
if (n.meta?.chapter) cIdx = Number(n.meta.chapter);
|
|
1218
|
+
starChapterIndices.push(cIdx);
|
|
890
1219
|
}
|
|
891
1220
|
if (n.level === 1 || n.level === 2 || n.level === 3) {
|
|
892
|
-
|
|
893
|
-
|
|
1221
|
+
let color = "#ffffff";
|
|
1222
|
+
if (n.level === 1) color = "#38bdf8";
|
|
1223
|
+
else if (n.level === 2) color = "#cbd5e1";
|
|
1224
|
+
else if (n.level === 3) color = "#94a3b8";
|
|
1225
|
+
let labelText = n.label;
|
|
1226
|
+
if (n.level === 3 && n.meta?.chapter) {
|
|
1227
|
+
labelText = String(n.meta.chapter);
|
|
1228
|
+
}
|
|
1229
|
+
const texRes = createTextTexture(labelText, color);
|
|
894
1230
|
if (texRes) {
|
|
895
1231
|
let baseScale = 0.05;
|
|
896
1232
|
if (n.level === 1) baseScale = 0.08;
|
|
897
|
-
else if (n.level ===
|
|
898
|
-
|
|
1233
|
+
else if (n.level === 2) baseScale = 0.04;
|
|
1234
|
+
else if (n.level === 3) baseScale = 0.03;
|
|
1235
|
+
const size = new THREE5__namespace.Vector2(baseScale * texRes.aspect, baseScale);
|
|
899
1236
|
const mat = createSmartMaterial({
|
|
900
1237
|
uniforms: {
|
|
901
1238
|
uMap: { value: texRes.tex },
|
|
@@ -936,7 +1273,7 @@ function createEngine({
|
|
|
936
1273
|
depthWrite: false,
|
|
937
1274
|
depthTest: true
|
|
938
1275
|
});
|
|
939
|
-
const mesh = new
|
|
1276
|
+
const mesh = new THREE5__namespace.Mesh(new THREE5__namespace.PlaneGeometry(1, 1), mat);
|
|
940
1277
|
let p = getPosition(n);
|
|
941
1278
|
if (n.level === 1) {
|
|
942
1279
|
if (divisionPositions.has(n.id)) {
|
|
@@ -946,7 +1283,8 @@ function createEngine({
|
|
|
946
1283
|
const angle = Math.atan2(p.z, p.x);
|
|
947
1284
|
p.set(r * Math.cos(angle), 150, r * Math.sin(angle));
|
|
948
1285
|
} else if (n.level === 3) {
|
|
949
|
-
p.
|
|
1286
|
+
p.y += 30;
|
|
1287
|
+
p.multiplyScalar(1.001);
|
|
950
1288
|
}
|
|
951
1289
|
mesh.position.set(p.x, p.y, p.z);
|
|
952
1290
|
mesh.scale.set(size.x, size.y, 1);
|
|
@@ -957,47 +1295,119 @@ function createEngine({
|
|
|
957
1295
|
}
|
|
958
1296
|
}
|
|
959
1297
|
}
|
|
960
|
-
const starGeo = new
|
|
961
|
-
starGeo.setAttribute("position", new
|
|
962
|
-
starGeo.setAttribute("size", new
|
|
963
|
-
starGeo.setAttribute("color", new
|
|
1298
|
+
const starGeo = new THREE5__namespace.BufferGeometry();
|
|
1299
|
+
starGeo.setAttribute("position", new THREE5__namespace.Float32BufferAttribute(starPositions, 3));
|
|
1300
|
+
starGeo.setAttribute("size", new THREE5__namespace.Float32BufferAttribute(starSizes, 1));
|
|
1301
|
+
starGeo.setAttribute("color", new THREE5__namespace.Float32BufferAttribute(starColors, 3));
|
|
1302
|
+
starGeo.setAttribute("phase", new THREE5__namespace.Float32BufferAttribute(starPhases, 1));
|
|
1303
|
+
starGeo.setAttribute("bookIndex", new THREE5__namespace.Float32BufferAttribute(starBookIndices, 1));
|
|
1304
|
+
starGeo.setAttribute("chapterIndex", new THREE5__namespace.Float32BufferAttribute(starChapterIndices, 1));
|
|
964
1305
|
const starMat = createSmartMaterial({
|
|
965
|
-
uniforms: {
|
|
1306
|
+
uniforms: {
|
|
1307
|
+
pixelRatio: { value: renderer.getPixelRatio() },
|
|
1308
|
+
uScale: globalUniforms.uScale,
|
|
1309
|
+
uTime: globalUniforms.uTime,
|
|
1310
|
+
uActiveBookIndex: { value: -1 },
|
|
1311
|
+
uOrderRevealStrength: { value: 0 },
|
|
1312
|
+
uGlobalDimFactor: { value: ORDER_REVEAL_CONFIG.globalDim },
|
|
1313
|
+
uPulseParams: { value: new THREE5__namespace.Vector3(
|
|
1314
|
+
ORDER_REVEAL_CONFIG.pulseDuration,
|
|
1315
|
+
ORDER_REVEAL_CONFIG.delayPerChapter,
|
|
1316
|
+
ORDER_REVEAL_CONFIG.pulseAmplitude
|
|
1317
|
+
) }
|
|
1318
|
+
},
|
|
966
1319
|
vertexShaderBody: `
|
|
967
1320
|
attribute float size;
|
|
968
1321
|
attribute vec3 color;
|
|
1322
|
+
attribute float phase;
|
|
1323
|
+
attribute float bookIndex;
|
|
1324
|
+
attribute float chapterIndex;
|
|
1325
|
+
|
|
969
1326
|
varying vec3 vColor;
|
|
970
1327
|
uniform float pixelRatio;
|
|
1328
|
+
|
|
1329
|
+
uniform float uTime;
|
|
1330
|
+
uniform float uAtmExtinction;
|
|
1331
|
+
uniform float uAtmTwinkle;
|
|
1332
|
+
|
|
1333
|
+
uniform float uActiveBookIndex;
|
|
1334
|
+
uniform float uOrderRevealStrength;
|
|
1335
|
+
uniform float uGlobalDimFactor;
|
|
1336
|
+
uniform vec3 uPulseParams;
|
|
1337
|
+
|
|
971
1338
|
void main() {
|
|
972
|
-
|
|
1339
|
+
vec3 nPos = normalize(position);
|
|
1340
|
+
|
|
1341
|
+
// 1. Altitude (Y is UP)
|
|
1342
|
+
float altitude = nPos.y;
|
|
1343
|
+
|
|
1344
|
+
// 2. Atmospheric Extinction (Airmass approximation)
|
|
1345
|
+
float airmass = 1.0 / (max(0.02, altitude + 0.05));
|
|
1346
|
+
float extinction = exp(-uAtmExtinction * 0.1 * airmass);
|
|
1347
|
+
|
|
1348
|
+
// Fade out stars below horizon
|
|
1349
|
+
float horizonFade = smoothstep(-0.1, 0.05, altitude);
|
|
1350
|
+
|
|
1351
|
+
// 3. Scintillation
|
|
1352
|
+
float turbulence = 1.0 + (1.0 - smoothstep(0.0, 1.0, altitude)) * 2.0;
|
|
1353
|
+
float twinkle = sin(uTime * 3.0 + phase + position.x * 0.01) * 0.5 + 0.5;
|
|
1354
|
+
float scintillation = mix(1.0, twinkle * 2.0, uAtmTwinkle * 0.5 * turbulence);
|
|
1355
|
+
|
|
1356
|
+
// --- Order Reveal Logic ---
|
|
1357
|
+
float isTarget = 1.0 - min(1.0, abs(bookIndex - uActiveBookIndex));
|
|
1358
|
+
|
|
1359
|
+
// Dimming
|
|
1360
|
+
float dimFactor = mix(1.0, uGlobalDimFactor, uOrderRevealStrength * (1.0 - isTarget));
|
|
1361
|
+
|
|
1362
|
+
// Pulse
|
|
1363
|
+
float delay = chapterIndex * uPulseParams.y;
|
|
1364
|
+
float cycleDuration = uPulseParams.x * 2.5;
|
|
1365
|
+
float t = mod(uTime - delay, cycleDuration);
|
|
1366
|
+
|
|
1367
|
+
float pulse = smoothstep(0.0, 0.2, t) * (1.0 - smoothstep(0.4, uPulseParams.x, t));
|
|
1368
|
+
pulse = max(0.0, pulse);
|
|
1369
|
+
|
|
1370
|
+
float activePulse = pulse * uPulseParams.z * isTarget * uOrderRevealStrength;
|
|
1371
|
+
|
|
1372
|
+
vec3 baseColor = color * extinction * horizonFade * scintillation;
|
|
1373
|
+
vColor = baseColor * dimFactor;
|
|
1374
|
+
vColor += vec3(1.0, 0.8, 0.4) * activePulse;
|
|
1375
|
+
|
|
973
1376
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
974
1377
|
gl_Position = smartProject(mvPosition);
|
|
975
1378
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
976
|
-
|
|
1379
|
+
|
|
1380
|
+
float sizeBoost = 1.0 + activePulse * 0.8;
|
|
1381
|
+
gl_PointSize = (size * sizeBoost * 1.5) * uScale * pixelRatio * (2000.0 / -mvPosition.z) * horizonFade;
|
|
977
1382
|
}
|
|
978
1383
|
`,
|
|
979
1384
|
fragmentShader: `
|
|
980
1385
|
varying vec3 vColor;
|
|
981
1386
|
void main() {
|
|
982
1387
|
vec2 coord = gl_PointCoord - vec2(0.5);
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
if (dist > 1.0) discard;
|
|
1388
|
+
float d = length(coord) * 2.0;
|
|
1389
|
+
if (d > 1.0) discard;
|
|
986
1390
|
|
|
987
1391
|
float alphaMask = getMaskAlpha();
|
|
988
1392
|
if (alphaMask < 0.01) discard;
|
|
989
1393
|
|
|
990
|
-
|
|
991
|
-
|
|
1394
|
+
float dd = d * d;
|
|
1395
|
+
// Stellarium Profile
|
|
1396
|
+
float core = exp(-20.0 * dd);
|
|
1397
|
+
float halo = exp(-4.0 * dd);
|
|
992
1398
|
|
|
993
|
-
|
|
1399
|
+
vec3 cCore = vec3(1.0) * core * 1.5;
|
|
1400
|
+
vec3 cHalo = vColor * halo * 0.6;
|
|
1401
|
+
|
|
1402
|
+
gl_FragColor = vec4((cCore + cHalo) * alphaMask, 1.0);
|
|
994
1403
|
}
|
|
995
1404
|
`,
|
|
996
1405
|
transparent: true,
|
|
997
1406
|
depthWrite: false,
|
|
998
|
-
depthTest: true
|
|
1407
|
+
depthTest: true,
|
|
1408
|
+
blending: THREE5__namespace.AdditiveBlending
|
|
999
1409
|
});
|
|
1000
|
-
starPoints = new
|
|
1410
|
+
starPoints = new THREE5__namespace.Points(starGeo, starMat);
|
|
1001
1411
|
starPoints.frustumCulled = false;
|
|
1002
1412
|
root.add(starPoints);
|
|
1003
1413
|
const linePoints = [];
|
|
@@ -1023,31 +1433,119 @@ function createEngine({
|
|
|
1023
1433
|
}
|
|
1024
1434
|
}
|
|
1025
1435
|
if (linePoints.length > 0) {
|
|
1026
|
-
const lineGeo = new
|
|
1027
|
-
lineGeo.setAttribute("position", new
|
|
1436
|
+
const lineGeo = new THREE5__namespace.BufferGeometry();
|
|
1437
|
+
lineGeo.setAttribute("position", new THREE5__namespace.Float32BufferAttribute(linePoints, 3));
|
|
1028
1438
|
const lineMat = createSmartMaterial({
|
|
1029
|
-
uniforms: { color: { value: new
|
|
1439
|
+
uniforms: { color: { value: new THREE5__namespace.Color(11193599) } },
|
|
1030
1440
|
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; }`,
|
|
1031
1441
|
fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.4 * alphaMask); }`,
|
|
1032
1442
|
transparent: true,
|
|
1033
1443
|
depthWrite: false,
|
|
1034
|
-
blending:
|
|
1444
|
+
blending: THREE5__namespace.AdditiveBlending
|
|
1035
1445
|
});
|
|
1036
|
-
constellationLines = new
|
|
1446
|
+
constellationLines = new THREE5__namespace.LineSegments(lineGeo, lineMat);
|
|
1037
1447
|
constellationLines.frustumCulled = false;
|
|
1038
1448
|
root.add(constellationLines);
|
|
1039
1449
|
}
|
|
1450
|
+
if (cfg.groups) {
|
|
1451
|
+
for (const [bookId, chapters] of bookMap.entries()) {
|
|
1452
|
+
const bookNode = nodeById.get(bookId);
|
|
1453
|
+
if (!bookNode) continue;
|
|
1454
|
+
const bookName = bookNode.meta?.book || bookNode.label;
|
|
1455
|
+
const groupList = cfg.groups[bookName.toLowerCase()];
|
|
1456
|
+
if (groupList) {
|
|
1457
|
+
groupList.forEach((g, idx) => {
|
|
1458
|
+
const groupId = `G:${bookId}:${idx}`;
|
|
1459
|
+
let p = new THREE5__namespace.Vector3();
|
|
1460
|
+
if (cfg.arrangement && cfg.arrangement[groupId]) {
|
|
1461
|
+
const arr = cfg.arrangement[groupId];
|
|
1462
|
+
p.set(arr.position[0], arr.position[1], arr.position[2]);
|
|
1463
|
+
} else {
|
|
1464
|
+
const relevantChapters = chapters.filter((c) => {
|
|
1465
|
+
const ch = c.meta?.chapter;
|
|
1466
|
+
return ch >= g.start && ch <= g.end;
|
|
1467
|
+
});
|
|
1468
|
+
if (relevantChapters.length === 0) return;
|
|
1469
|
+
for (const c of relevantChapters) {
|
|
1470
|
+
p.add(getPosition(c));
|
|
1471
|
+
}
|
|
1472
|
+
p.divideScalar(relevantChapters.length);
|
|
1473
|
+
}
|
|
1474
|
+
const labelText = `${g.name} (${g.start}-${g.end})`;
|
|
1475
|
+
const texRes = createTextTexture(labelText, "#4fa4fa80");
|
|
1476
|
+
if (texRes) {
|
|
1477
|
+
const baseScale = 0.036;
|
|
1478
|
+
const size = new THREE5__namespace.Vector2(baseScale * texRes.aspect, baseScale);
|
|
1479
|
+
const mat = createSmartMaterial({
|
|
1480
|
+
uniforms: {
|
|
1481
|
+
uMap: { value: texRes.tex },
|
|
1482
|
+
uSize: { value: size },
|
|
1483
|
+
uAlpha: { value: 0 },
|
|
1484
|
+
uAngle: { value: 0 }
|
|
1485
|
+
},
|
|
1486
|
+
vertexShaderBody: `
|
|
1487
|
+
uniform vec2 uSize;
|
|
1488
|
+
uniform float uAngle;
|
|
1489
|
+
varying vec2 vUv;
|
|
1490
|
+
void main() {
|
|
1491
|
+
vUv = uv;
|
|
1492
|
+
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
1493
|
+
vec4 projected = smartProject(mvPos);
|
|
1494
|
+
|
|
1495
|
+
float c = cos(uAngle);
|
|
1496
|
+
float s = sin(uAngle);
|
|
1497
|
+
mat2 rot = mat2(c, -s, s, c);
|
|
1498
|
+
vec2 offset = rot * (position.xy * uSize);
|
|
1499
|
+
|
|
1500
|
+
projected.xy += offset / vec2(uAspect, 1.0);
|
|
1501
|
+
gl_Position = projected;
|
|
1502
|
+
}
|
|
1503
|
+
`,
|
|
1504
|
+
fragmentShader: `
|
|
1505
|
+
uniform sampler2D uMap;
|
|
1506
|
+
uniform float uAlpha;
|
|
1507
|
+
varying vec2 vUv;
|
|
1508
|
+
void main() {
|
|
1509
|
+
float mask = getMaskAlpha();
|
|
1510
|
+
if (mask < 0.01) discard;
|
|
1511
|
+
vec4 tex = texture2D(uMap, vUv);
|
|
1512
|
+
gl_FragColor = vec4(tex.rgb, tex.a * uAlpha * mask);
|
|
1513
|
+
}
|
|
1514
|
+
`,
|
|
1515
|
+
transparent: true,
|
|
1516
|
+
depthWrite: false,
|
|
1517
|
+
depthTest: true
|
|
1518
|
+
});
|
|
1519
|
+
const mesh = new THREE5__namespace.Mesh(new THREE5__namespace.PlaneGeometry(1, 1), mat);
|
|
1520
|
+
mesh.position.copy(p);
|
|
1521
|
+
mesh.scale.set(size.x, size.y, 1);
|
|
1522
|
+
mesh.frustumCulled = false;
|
|
1523
|
+
mesh.userData = { id: groupId };
|
|
1524
|
+
root.add(mesh);
|
|
1525
|
+
const node = {
|
|
1526
|
+
id: groupId,
|
|
1527
|
+
label: labelText,
|
|
1528
|
+
level: 2.5,
|
|
1529
|
+
// Special Level
|
|
1530
|
+
parent: bookId
|
|
1531
|
+
};
|
|
1532
|
+
dynamicLabels.push({ obj: mesh, node, initialScale: size.clone() });
|
|
1533
|
+
}
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1040
1538
|
const boundaries = laidOut.meta?.divisionBoundaries ?? [];
|
|
1041
1539
|
if (boundaries.length > 0) {
|
|
1042
1540
|
const boundaryMat = createSmartMaterial({
|
|
1043
|
-
uniforms: { color: { value: new
|
|
1541
|
+
uniforms: { color: { value: new THREE5__namespace.Color(5601177) } },
|
|
1044
1542
|
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; }`,
|
|
1045
1543
|
fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.10 * alphaMask); }`,
|
|
1046
1544
|
transparent: true,
|
|
1047
1545
|
depthWrite: false,
|
|
1048
|
-
blending:
|
|
1546
|
+
blending: THREE5__namespace.AdditiveBlending
|
|
1049
1547
|
});
|
|
1050
|
-
const boundaryGeo = new
|
|
1548
|
+
const boundaryGeo = new THREE5__namespace.BufferGeometry();
|
|
1051
1549
|
const bPoints = [];
|
|
1052
1550
|
boundaries.forEach((angle) => {
|
|
1053
1551
|
const steps = 32;
|
|
@@ -1060,8 +1558,8 @@ function createEngine({
|
|
|
1060
1558
|
bPoints.push(p2.x, p2.y, p2.z);
|
|
1061
1559
|
}
|
|
1062
1560
|
});
|
|
1063
|
-
boundaryGeo.setAttribute("position", new
|
|
1064
|
-
boundaryLines = new
|
|
1561
|
+
boundaryGeo.setAttribute("position", new THREE5__namespace.Float32BufferAttribute(bPoints, 3));
|
|
1562
|
+
boundaryLines = new THREE5__namespace.LineSegments(boundaryGeo, boundaryMat);
|
|
1065
1563
|
boundaryLines.frustumCulled = false;
|
|
1066
1564
|
root.add(boundaryLines);
|
|
1067
1565
|
}
|
|
@@ -1080,7 +1578,7 @@ function createEngine({
|
|
|
1080
1578
|
const r_norm = Math.sqrt(x * x + y * y);
|
|
1081
1579
|
const phi = Math.atan2(y, x);
|
|
1082
1580
|
const theta = r_norm * (Math.PI / 2);
|
|
1083
|
-
return new
|
|
1581
|
+
return new THREE5__namespace.Vector3(
|
|
1084
1582
|
Math.sin(theta) * Math.cos(phi),
|
|
1085
1583
|
Math.cos(theta),
|
|
1086
1584
|
Math.sin(theta) * Math.sin(phi)
|
|
@@ -1093,18 +1591,18 @@ function createEngine({
|
|
|
1093
1591
|
}
|
|
1094
1592
|
}
|
|
1095
1593
|
if (polyPoints.length > 0) {
|
|
1096
|
-
const polyGeo = new
|
|
1097
|
-
polyGeo.setAttribute("position", new
|
|
1594
|
+
const polyGeo = new THREE5__namespace.BufferGeometry();
|
|
1595
|
+
polyGeo.setAttribute("position", new THREE5__namespace.Float32BufferAttribute(polyPoints, 3));
|
|
1098
1596
|
const polyMat = createSmartMaterial({
|
|
1099
|
-
uniforms: { color: { value: new
|
|
1597
|
+
uniforms: { color: { value: new THREE5__namespace.Color(3718648) } },
|
|
1100
1598
|
// Cyan-ish
|
|
1101
1599
|
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; }`,
|
|
1102
1600
|
fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.2 * alphaMask); }`,
|
|
1103
1601
|
transparent: true,
|
|
1104
1602
|
depthWrite: false,
|
|
1105
|
-
blending:
|
|
1603
|
+
blending: THREE5__namespace.AdditiveBlending
|
|
1106
1604
|
});
|
|
1107
|
-
const polyLines = new
|
|
1605
|
+
const polyLines = new THREE5__namespace.LineSegments(polyGeo, polyMat);
|
|
1108
1606
|
polyLines.frustumCulled = false;
|
|
1109
1607
|
root.add(polyLines);
|
|
1110
1608
|
}
|
|
@@ -1116,6 +1614,7 @@ function createEngine({
|
|
|
1116
1614
|
let lastModel = void 0;
|
|
1117
1615
|
let lastAppliedLon = void 0;
|
|
1118
1616
|
let lastAppliedLat = void 0;
|
|
1617
|
+
let lastBackdropCount = void 0;
|
|
1119
1618
|
function setConfig(cfg) {
|
|
1120
1619
|
currentConfig = cfg;
|
|
1121
1620
|
if (typeof cfg.camera?.lon === "number" && cfg.camera.lon !== lastAppliedLon) {
|
|
@@ -1128,6 +1627,11 @@ function createEngine({
|
|
|
1128
1627
|
state.targetLat = cfg.camera.lat;
|
|
1129
1628
|
lastAppliedLat = cfg.camera.lat;
|
|
1130
1629
|
}
|
|
1630
|
+
const desiredBackdropCount = typeof cfg.backdropStarsCount === "number" ? cfg.backdropStarsCount : 4e3;
|
|
1631
|
+
if (lastBackdropCount !== desiredBackdropCount) {
|
|
1632
|
+
createBackdropStars(desiredBackdropCount);
|
|
1633
|
+
lastBackdropCount = desiredBackdropCount;
|
|
1634
|
+
}
|
|
1131
1635
|
let shouldRebuild = false;
|
|
1132
1636
|
let model = cfg.model;
|
|
1133
1637
|
if (!model && cfg.data && cfg.adapter) {
|
|
@@ -1151,6 +1655,29 @@ function createEngine({
|
|
|
1151
1655
|
} else if (cfg.arrangement && starPoints) {
|
|
1152
1656
|
if (lastModel) buildFromModel(lastModel, cfg);
|
|
1153
1657
|
}
|
|
1658
|
+
if (cfg.constellations) {
|
|
1659
|
+
constellationLayer.load(cfg.constellations, (id) => {
|
|
1660
|
+
if (cfg.arrangement && cfg.arrangement[id]) {
|
|
1661
|
+
const arr = cfg.arrangement[id];
|
|
1662
|
+
if (arr.position[2] === 0) {
|
|
1663
|
+
const x = arr.position[0];
|
|
1664
|
+
const y = arr.position[1];
|
|
1665
|
+
const radius = cfg.layout?.radius ?? 2e3;
|
|
1666
|
+
const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
|
|
1667
|
+
const phi = Math.atan2(y, x);
|
|
1668
|
+
const theta = r_norm * (Math.PI / 2);
|
|
1669
|
+
return new THREE5__namespace.Vector3(
|
|
1670
|
+
Math.sin(theta) * Math.cos(phi),
|
|
1671
|
+
Math.cos(theta),
|
|
1672
|
+
Math.sin(theta) * Math.sin(phi)
|
|
1673
|
+
).multiplyScalar(radius);
|
|
1674
|
+
}
|
|
1675
|
+
return new THREE5__namespace.Vector3(arr.position[0], arr.position[1], arr.position[2]);
|
|
1676
|
+
}
|
|
1677
|
+
const n = nodeById.get(id);
|
|
1678
|
+
return n ? getPosition(n) : null;
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1154
1681
|
}
|
|
1155
1682
|
function setHandlers(next) {
|
|
1156
1683
|
handlers = next;
|
|
@@ -1170,8 +1697,12 @@ function createEngine({
|
|
|
1170
1697
|
}
|
|
1171
1698
|
}
|
|
1172
1699
|
for (const item of dynamicLabels) {
|
|
1700
|
+
if (item.node.level === 3) continue;
|
|
1173
1701
|
arr[item.node.id] = { position: [item.obj.position.x, item.obj.position.y, item.obj.position.z] };
|
|
1174
1702
|
}
|
|
1703
|
+
for (const item of constellationLayer.getItems()) {
|
|
1704
|
+
arr[item.config.id] = { position: [item.mesh.position.x, item.mesh.position.y, item.mesh.position.z] };
|
|
1705
|
+
}
|
|
1175
1706
|
Object.assign(arr, state.tempArrangement);
|
|
1176
1707
|
return arr;
|
|
1177
1708
|
}
|
|
@@ -1181,16 +1712,18 @@ function createEngine({
|
|
|
1181
1712
|
const mY = ev.clientY - rect.top;
|
|
1182
1713
|
mouseNDC.x = mX / rect.width * 2 - 1;
|
|
1183
1714
|
mouseNDC.y = -(mY / rect.height) * 2 + 1;
|
|
1184
|
-
let closestLabel = null;
|
|
1185
|
-
let minLabelDist = 40;
|
|
1186
1715
|
const uScale = globalUniforms.uScale.value;
|
|
1187
1716
|
const uAspect = camera.aspect;
|
|
1188
1717
|
const w = rect.width;
|
|
1189
1718
|
const h = rect.height;
|
|
1719
|
+
let closestLabel = null;
|
|
1720
|
+
let minLabelDist = 40;
|
|
1190
1721
|
for (const item of dynamicLabels) {
|
|
1191
1722
|
if (!item.obj.visible) continue;
|
|
1192
1723
|
const pWorld = item.obj.position;
|
|
1193
1724
|
const pProj = smartProjectJS(pWorld);
|
|
1725
|
+
const isBehind = globalUniforms.uBlend.value > 0.5 && pProj.z > 0.4 || globalUniforms.uBlend.value < 0.1 && pProj.z > -0.1;
|
|
1726
|
+
if (isBehind) continue;
|
|
1194
1727
|
const xNDC = pProj.x * uScale / uAspect;
|
|
1195
1728
|
const yNDC = pProj.y * uScale;
|
|
1196
1729
|
const sX = (xNDC * 0.5 + 0.5) * w;
|
|
@@ -1198,24 +1731,72 @@ function createEngine({
|
|
|
1198
1731
|
const dx = mX - sX;
|
|
1199
1732
|
const dy = mY - sY;
|
|
1200
1733
|
const d = Math.sqrt(dx * dx + dy * dy);
|
|
1201
|
-
|
|
1202
|
-
if (!isBehind && d < minLabelDist) {
|
|
1734
|
+
if (d < minLabelDist) {
|
|
1203
1735
|
minLabelDist = d;
|
|
1204
1736
|
closestLabel = item;
|
|
1205
1737
|
}
|
|
1206
1738
|
}
|
|
1207
|
-
if (closestLabel)
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
const
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1739
|
+
if (closestLabel) {
|
|
1740
|
+
return { type: "label", node: closestLabel.node, object: closestLabel.obj, point: closestLabel.obj.position.clone(), index: void 0 };
|
|
1741
|
+
}
|
|
1742
|
+
let closestConst = null;
|
|
1743
|
+
let minConstDist = Infinity;
|
|
1744
|
+
for (const item of constellationLayer.getItems()) {
|
|
1745
|
+
if (!item.mesh.visible) continue;
|
|
1746
|
+
const pWorld = item.mesh.position;
|
|
1747
|
+
const pProj = smartProjectJS(pWorld);
|
|
1748
|
+
const isBehind = globalUniforms.uBlend.value > 0.5 && pProj.z > 0.4 || globalUniforms.uBlend.value < 0.1 && pProj.z > -0.1;
|
|
1749
|
+
if (isBehind) continue;
|
|
1750
|
+
const uniforms = item.material.uniforms;
|
|
1751
|
+
if (!uniforms || !uniforms.uSize) continue;
|
|
1752
|
+
const uSize = uniforms.uSize.value;
|
|
1753
|
+
const uImgAspect = uniforms.uImgAspect.value;
|
|
1754
|
+
const uImgRotation = uniforms.uImgRotation.value;
|
|
1755
|
+
const dist = pWorld.length();
|
|
1756
|
+
if (dist < 1e-3) continue;
|
|
1757
|
+
const scale = uSize / dist * uScale;
|
|
1758
|
+
const halfH_px = scale / 2 * (h / 2);
|
|
1759
|
+
const halfW_px = halfH_px * uImgAspect;
|
|
1760
|
+
const xNDC = pProj.x * uScale / uAspect;
|
|
1761
|
+
const yNDC = pProj.y * uScale;
|
|
1762
|
+
const sX = (xNDC * 0.5 + 0.5) * w;
|
|
1763
|
+
const sY = (-yNDC * 0.5 + 0.5) * h;
|
|
1764
|
+
const dx = mX - sX;
|
|
1765
|
+
const dy = mY - sY;
|
|
1766
|
+
const dy_cart = -dy;
|
|
1767
|
+
const cr = Math.cos(-uImgRotation);
|
|
1768
|
+
const sr = Math.sin(-uImgRotation);
|
|
1769
|
+
const localX = dx * cr - dy_cart * sr;
|
|
1770
|
+
const localY = dx * sr + dy_cart * cr;
|
|
1771
|
+
if (Math.abs(localX) < halfW_px * 1.2 && Math.abs(localY) < halfH_px * 1.2) {
|
|
1772
|
+
const d = Math.sqrt(dx * dx + dy * dy);
|
|
1773
|
+
if (!closestConst || d < minConstDist) {
|
|
1774
|
+
minConstDist = d;
|
|
1775
|
+
closestConst = item;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
if (closestConst) {
|
|
1780
|
+
const fakeNode = {
|
|
1781
|
+
id: closestConst.config.id,
|
|
1782
|
+
label: closestConst.config.title,
|
|
1783
|
+
level: -1
|
|
1784
|
+
};
|
|
1785
|
+
return { type: "constellation", node: fakeNode, object: closestConst.mesh, point: closestConst.mesh.position.clone(), index: void 0 };
|
|
1786
|
+
}
|
|
1787
|
+
if (starPoints) {
|
|
1788
|
+
const worldDir = getMouseWorldVector(mX, mY, rect.width, rect.height);
|
|
1789
|
+
raycaster.ray.origin.set(0, 0, 0);
|
|
1790
|
+
raycaster.ray.direction.copy(worldDir);
|
|
1791
|
+
raycaster.params.Points.threshold = 5 * (state.fov / 60);
|
|
1792
|
+
const hits = raycaster.intersectObject(starPoints, false);
|
|
1793
|
+
const pointHit = hits[0];
|
|
1794
|
+
if (pointHit && pointHit.index !== void 0) {
|
|
1795
|
+
const id = starIndexToId[pointHit.index];
|
|
1796
|
+
if (id) {
|
|
1797
|
+
const node = nodeById.get(id);
|
|
1798
|
+
if (node) return { type: "star", node, index: pointHit.index, point: pointHit.point, object: void 0 };
|
|
1799
|
+
}
|
|
1219
1800
|
}
|
|
1220
1801
|
}
|
|
1221
1802
|
return void 0;
|
|
@@ -1243,16 +1824,19 @@ function createEngine({
|
|
|
1243
1824
|
if (starId) {
|
|
1244
1825
|
const starNode = nodeById.get(starId);
|
|
1245
1826
|
if (starNode && starNode.parent === bookId) {
|
|
1246
|
-
children.push({ index: i, initialPos: new
|
|
1827
|
+
children.push({ index: i, initialPos: new THREE5__namespace.Vector3(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]) });
|
|
1247
1828
|
}
|
|
1248
1829
|
}
|
|
1249
1830
|
}
|
|
1250
1831
|
}
|
|
1251
1832
|
state.draggedGroup = { labelInitialPos: hit.object.position.clone(), children };
|
|
1252
1833
|
state.draggedStarIndex = -1;
|
|
1834
|
+
} else if (hit.type === "constellation") {
|
|
1835
|
+
state.draggedGroup = null;
|
|
1836
|
+
state.draggedStarIndex = -1;
|
|
1253
1837
|
}
|
|
1254
|
-
return;
|
|
1255
1838
|
}
|
|
1839
|
+
return;
|
|
1256
1840
|
}
|
|
1257
1841
|
state.dragMode = "camera";
|
|
1258
1842
|
state.isDragging = true;
|
|
@@ -1282,13 +1866,19 @@ function createEngine({
|
|
|
1282
1866
|
if (item) {
|
|
1283
1867
|
item.obj.position.copy(newPos);
|
|
1284
1868
|
state.tempArrangement[item.node.id] = { position: [newPos.x, newPos.y, newPos.z] };
|
|
1869
|
+
} else if (state.draggedNodeId) {
|
|
1870
|
+
const cItem = constellationLayer.getItems().find((c) => c.config.id === state.draggedNodeId);
|
|
1871
|
+
if (cItem) {
|
|
1872
|
+
cItem.mesh.position.copy(newPos);
|
|
1873
|
+
state.tempArrangement[state.draggedNodeId] = { position: [newPos.x, newPos.y, newPos.z] };
|
|
1874
|
+
}
|
|
1285
1875
|
}
|
|
1286
1876
|
const vStart = group.labelInitialPos.clone().normalize();
|
|
1287
1877
|
const vEnd = newPos.clone().normalize();
|
|
1288
|
-
const q = new
|
|
1878
|
+
const q = new THREE5__namespace.Quaternion().setFromUnitVectors(vStart, vEnd);
|
|
1289
1879
|
if (starPoints && group.children.length > 0) {
|
|
1290
1880
|
const attr = starPoints.geometry.attributes.position;
|
|
1291
|
-
const tempVec = new
|
|
1881
|
+
const tempVec = new THREE5__namespace.Vector3();
|
|
1292
1882
|
for (const child of group.children) {
|
|
1293
1883
|
tempVec.copy(child.initialPos).applyQuaternion(q);
|
|
1294
1884
|
attr.setXYZ(child.index, tempVec.x, tempVec.y, tempVec.z);
|
|
@@ -1322,7 +1912,7 @@ function createEngine({
|
|
|
1322
1912
|
if (res) {
|
|
1323
1913
|
hoverLabelMat.uniforms.uMap.value = res.tex;
|
|
1324
1914
|
const baseScale = 0.03;
|
|
1325
|
-
const size = new
|
|
1915
|
+
const size = new THREE5__namespace.Vector2(baseScale * res.aspect, baseScale);
|
|
1326
1916
|
hoverLabelMat.uniforms.uSize.value = size;
|
|
1327
1917
|
hoverLabelMesh.scale.set(size.x, size.y, 1);
|
|
1328
1918
|
}
|
|
@@ -1338,6 +1928,7 @@ function createEngine({
|
|
|
1338
1928
|
if (hit?.node.id !== handlers._lastHoverId) {
|
|
1339
1929
|
handlers._lastHoverId = hit?.node.id;
|
|
1340
1930
|
handlers.onHover?.(hit?.node);
|
|
1931
|
+
constellationLayer.setHovered(hit?.node.id ?? null);
|
|
1341
1932
|
}
|
|
1342
1933
|
document.body.style.cursor = hit ? currentConfig?.editable ? "crosshair" : "pointer" : "default";
|
|
1343
1934
|
}
|
|
@@ -1357,7 +1948,14 @@ function createEngine({
|
|
|
1357
1948
|
document.body.style.cursor = "default";
|
|
1358
1949
|
} else {
|
|
1359
1950
|
const hit = pick(e);
|
|
1360
|
-
if (hit)
|
|
1951
|
+
if (hit) {
|
|
1952
|
+
handlers.onSelect?.(hit.node);
|
|
1953
|
+
constellationLayer.setFocused(hit.node.id);
|
|
1954
|
+
if (hit.node.level === 2) setFocusedBook(hit.node.id);
|
|
1955
|
+
else if (hit.node.level === 3 && hit.node.parent) setFocusedBook(hit.node.parent);
|
|
1956
|
+
} else {
|
|
1957
|
+
setFocusedBook(null);
|
|
1958
|
+
}
|
|
1361
1959
|
}
|
|
1362
1960
|
}
|
|
1363
1961
|
function onWheel(e) {
|
|
@@ -1368,25 +1966,26 @@ function createEngine({
|
|
|
1368
1966
|
const zoomSpeed = 1e-3 * state.fov;
|
|
1369
1967
|
state.fov += e.deltaY * zoomSpeed;
|
|
1370
1968
|
state.fov = Math.max(ENGINE_CONFIG.minFov, Math.min(ENGINE_CONFIG.maxFov, state.fov));
|
|
1969
|
+
handlers.onFovChange?.(state.fov);
|
|
1371
1970
|
updateUniforms();
|
|
1372
1971
|
const vAfter = getMouseViewVector(state.fov, aspect);
|
|
1373
|
-
const quaternion = new
|
|
1972
|
+
const quaternion = new THREE5__namespace.Quaternion().setFromUnitVectors(vAfter, vBefore);
|
|
1374
1973
|
const y = Math.sin(state.lat);
|
|
1375
1974
|
const r = Math.cos(state.lat);
|
|
1376
1975
|
const x = r * Math.sin(state.lon);
|
|
1377
1976
|
const z = -r * Math.cos(state.lon);
|
|
1378
|
-
const currentLook = new
|
|
1977
|
+
const currentLook = new THREE5__namespace.Vector3(x, y, z);
|
|
1379
1978
|
const camForward = currentLook.clone().normalize();
|
|
1380
1979
|
const camUp = camera.up.clone();
|
|
1381
|
-
const camRight = new
|
|
1382
|
-
const camUpOrtho = new
|
|
1383
|
-
const mat = new
|
|
1384
|
-
const qOld = new
|
|
1980
|
+
const camRight = new THREE5__namespace.Vector3().crossVectors(camForward, camUp).normalize();
|
|
1981
|
+
const camUpOrtho = new THREE5__namespace.Vector3().crossVectors(camRight, camForward).normalize();
|
|
1982
|
+
const mat = new THREE5__namespace.Matrix4().makeBasis(camRight, camUpOrtho, camForward.clone().negate());
|
|
1983
|
+
const qOld = new THREE5__namespace.Quaternion().setFromRotationMatrix(mat);
|
|
1385
1984
|
const qNew = qOld.clone().multiply(quaternion);
|
|
1386
|
-
const newForward = new
|
|
1985
|
+
const newForward = new THREE5__namespace.Vector3(0, 0, -1).applyQuaternion(qNew);
|
|
1387
1986
|
state.lat = Math.asin(Math.max(-0.999, Math.min(0.999, newForward.y)));
|
|
1388
1987
|
state.lon = Math.atan2(newForward.x, -newForward.z);
|
|
1389
|
-
const newUp = new
|
|
1988
|
+
const newUp = new THREE5__namespace.Vector3(0, 1, 0).applyQuaternion(qNew);
|
|
1390
1989
|
camera.up.copy(newUp);
|
|
1391
1990
|
if (e.deltaY > 0 && state.fov > ENGINE_CONFIG.zenithStartFov) {
|
|
1392
1991
|
const range = ENGINE_CONFIG.maxFov - ENGINE_CONFIG.zenithStartFov;
|
|
@@ -1427,55 +2026,89 @@ function createEngine({
|
|
|
1427
2026
|
function tick() {
|
|
1428
2027
|
if (!running) return;
|
|
1429
2028
|
raf = requestAnimationFrame(tick);
|
|
1430
|
-
|
|
2029
|
+
const now = performance.now();
|
|
2030
|
+
globalUniforms.uTime.value = now / 1e3;
|
|
2031
|
+
let activeId = null;
|
|
2032
|
+
if (focusedBookId) {
|
|
2033
|
+
activeId = focusedBookId;
|
|
2034
|
+
} else if (hoveredBookId) {
|
|
2035
|
+
const lastExit = hoverCooldowns.get(hoveredBookId) || 0;
|
|
2036
|
+
if (now - lastExit > COOLDOWN_MS) {
|
|
2037
|
+
activeId = hoveredBookId;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
const targetStrength = orderRevealEnabled && activeId ? 1 : 0;
|
|
2041
|
+
orderRevealStrength = mix(orderRevealStrength, targetStrength, 0.1);
|
|
2042
|
+
if (orderRevealStrength > 1e-3 || targetStrength > 0) {
|
|
2043
|
+
if (activeId && bookIdToIndex.has(activeId)) {
|
|
2044
|
+
activeBookIndex = bookIdToIndex.get(activeId);
|
|
2045
|
+
}
|
|
2046
|
+
if (starPoints && starPoints.material) {
|
|
2047
|
+
const m = starPoints.material;
|
|
2048
|
+
if (m.uniforms.uActiveBookIndex) m.uniforms.uActiveBookIndex.value = activeBookIndex;
|
|
2049
|
+
if (m.uniforms.uOrderRevealStrength) m.uniforms.uOrderRevealStrength.value = orderRevealStrength;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
let panX = 0;
|
|
2053
|
+
let panY = 0;
|
|
2054
|
+
if (!state.isDragging && isMouseInWindow && !currentConfig?.editable) {
|
|
1431
2055
|
const t = ENGINE_CONFIG.edgePanThreshold;
|
|
1432
|
-
const
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
state.targetLat = state.lat;
|
|
2056
|
+
const inZoneX = mouseNDC.x < -1 + t || mouseNDC.x > 1 - t;
|
|
2057
|
+
const inZoneY = mouseNDC.y < -1 + t || mouseNDC.y > 1 - t;
|
|
2058
|
+
if (inZoneX || inZoneY) {
|
|
2059
|
+
if (edgeHoverStart === 0) edgeHoverStart = performance.now();
|
|
2060
|
+
if (performance.now() - edgeHoverStart > ENGINE_CONFIG.edgePanDelay) {
|
|
2061
|
+
const speedBase = ENGINE_CONFIG.edgePanMaxSpeed * (state.fov / ENGINE_CONFIG.defaultFov);
|
|
2062
|
+
if (mouseNDC.x < -1 + t) {
|
|
2063
|
+
const s = (-1 + t - mouseNDC.x) / t;
|
|
2064
|
+
panX = -s * s * speedBase;
|
|
2065
|
+
} else if (mouseNDC.x > 1 - t) {
|
|
2066
|
+
const s = (mouseNDC.x - (1 - t)) / t;
|
|
2067
|
+
panX = s * s * speedBase;
|
|
2068
|
+
}
|
|
2069
|
+
if (mouseNDC.y < -1 + t) {
|
|
2070
|
+
const s = (-1 + t - mouseNDC.y) / t;
|
|
2071
|
+
panY = -s * s * speedBase;
|
|
2072
|
+
} else if (mouseNDC.y > 1 - t) {
|
|
2073
|
+
const s = (mouseNDC.y - (1 - t)) / t;
|
|
2074
|
+
panY = s * s * speedBase;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
1454
2077
|
} else {
|
|
1455
|
-
|
|
1456
|
-
state.lat += state.velocityY;
|
|
1457
|
-
state.velocityX *= ENGINE_CONFIG.inertiaDamping;
|
|
1458
|
-
state.velocityY *= ENGINE_CONFIG.inertiaDamping;
|
|
1459
|
-
if (Math.abs(state.velocityX) < 1e-6) state.velocityX = 0;
|
|
1460
|
-
if (Math.abs(state.velocityY) < 1e-6) state.velocityY = 0;
|
|
2078
|
+
edgeHoverStart = 0;
|
|
1461
2079
|
}
|
|
2080
|
+
} else {
|
|
2081
|
+
edgeHoverStart = 0;
|
|
2082
|
+
}
|
|
2083
|
+
if (Math.abs(panX) > 0 || Math.abs(panY) > 0) {
|
|
2084
|
+
state.lon += panX;
|
|
2085
|
+
state.lat += panY;
|
|
2086
|
+
state.targetLon = state.lon;
|
|
2087
|
+
state.targetLat = state.lat;
|
|
1462
2088
|
} else if (!state.isDragging) {
|
|
1463
2089
|
state.lon += state.velocityX;
|
|
1464
2090
|
state.lat += state.velocityY;
|
|
1465
2091
|
state.velocityX *= ENGINE_CONFIG.inertiaDamping;
|
|
1466
2092
|
state.velocityY *= ENGINE_CONFIG.inertiaDamping;
|
|
2093
|
+
if (Math.abs(state.velocityX) < 1e-6) state.velocityX = 0;
|
|
2094
|
+
if (Math.abs(state.velocityY) < 1e-6) state.velocityY = 0;
|
|
1467
2095
|
}
|
|
1468
2096
|
state.lat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.lat));
|
|
1469
2097
|
const y = Math.sin(state.lat);
|
|
1470
2098
|
const r = Math.cos(state.lat);
|
|
1471
2099
|
const x = r * Math.sin(state.lon);
|
|
1472
2100
|
const z = -r * Math.cos(state.lon);
|
|
1473
|
-
const target = new
|
|
1474
|
-
const idealUp = new
|
|
2101
|
+
const target = new THREE5__namespace.Vector3(x, y, z);
|
|
2102
|
+
const idealUp = new THREE5__namespace.Vector3(-Math.sin(state.lat) * Math.sin(state.lon), Math.cos(state.lat), Math.sin(state.lat) * Math.cos(state.lon)).normalize();
|
|
1475
2103
|
camera.up.lerp(idealUp, ENGINE_CONFIG.horizonLockStrength);
|
|
1476
2104
|
camera.up.normalize();
|
|
1477
2105
|
camera.lookAt(target);
|
|
2106
|
+
camera.updateMatrixWorld();
|
|
2107
|
+
camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
|
|
1478
2108
|
updateUniforms();
|
|
2109
|
+
constellationLayer.update(state.fov, currentConfig?.showConstellationArt ?? false);
|
|
2110
|
+
backdropGroup.visible = currentConfig?.showBackdropStars ?? true;
|
|
2111
|
+
if (atmosphereMesh) atmosphereMesh.visible = currentConfig?.showAtmosphere ?? false;
|
|
1479
2112
|
const DIVISION_THRESHOLD = 60;
|
|
1480
2113
|
const showDivisions = state.fov > DIVISION_THRESHOLD;
|
|
1481
2114
|
if (constellationLines) {
|
|
@@ -1499,7 +2132,9 @@ function createEngine({
|
|
|
1499
2132
|
const showBookLabels = currentConfig?.showBookLabels === true;
|
|
1500
2133
|
const showDivisionLabels = currentConfig?.showDivisionLabels === true;
|
|
1501
2134
|
const showChapterLabels = currentConfig?.showChapterLabels === true;
|
|
1502
|
-
const
|
|
2135
|
+
const showGroupLabels = currentConfig?.showGroupLabels === true;
|
|
2136
|
+
const showBooks = state.fov < 120;
|
|
2137
|
+
const showChapters = state.fov < 70;
|
|
1503
2138
|
for (const item of dynamicLabels) {
|
|
1504
2139
|
const uniforms = item.obj.material.uniforms;
|
|
1505
2140
|
const level = item.node.level;
|
|
@@ -1507,20 +2142,26 @@ function createEngine({
|
|
|
1507
2142
|
if (level === 2 && showBookLabels) isEnabled = true;
|
|
1508
2143
|
else if (level === 1 && showDivisionLabels) isEnabled = true;
|
|
1509
2144
|
else if (level === 3 && showChapterLabels) isEnabled = true;
|
|
2145
|
+
else if (level === 2.5 && showGroupLabels) isEnabled = true;
|
|
1510
2146
|
if (!isEnabled) {
|
|
1511
|
-
uniforms.uAlpha.value =
|
|
2147
|
+
uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
1512
2148
|
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
1513
2149
|
continue;
|
|
1514
2150
|
}
|
|
1515
2151
|
const pWorld = item.obj.position;
|
|
1516
2152
|
const pProj = smartProjectJS(pWorld);
|
|
1517
2153
|
if (pProj.z > 0.2) {
|
|
1518
|
-
uniforms.uAlpha.value =
|
|
2154
|
+
uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
1519
2155
|
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
1520
2156
|
continue;
|
|
1521
2157
|
}
|
|
1522
|
-
if (level ===
|
|
1523
|
-
uniforms.uAlpha.value =
|
|
2158
|
+
if (level === 2 && !showBooks && item.node.id !== state.draggedNodeId) {
|
|
2159
|
+
uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
2160
|
+
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
2161
|
+
continue;
|
|
2162
|
+
}
|
|
2163
|
+
if ((level === 3 || level === 2.5) && !showChapters && item.node.id !== state.draggedNodeId) {
|
|
2164
|
+
uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
1524
2165
|
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
1525
2166
|
continue;
|
|
1526
2167
|
}
|
|
@@ -1531,7 +2172,7 @@ function createEngine({
|
|
|
1531
2172
|
const size = uniforms.uSize.value;
|
|
1532
2173
|
const pixelH = size.y * screenH * 0.8;
|
|
1533
2174
|
const pixelW = size.x * screenH * 0.8;
|
|
1534
|
-
labelsToCheck.push({ item, sX, sY, w: pixelW, h: pixelH, uniforms, level });
|
|
2175
|
+
labelsToCheck.push({ item, sX, sY, w: pixelW, h: pixelH, uniforms, level, ndcX, ndcY });
|
|
1535
2176
|
}
|
|
1536
2177
|
const hoverId = handlers._lastHoverId;
|
|
1537
2178
|
const selectedId = state.draggedNodeId;
|
|
@@ -1557,11 +2198,13 @@ function createEngine({
|
|
|
1557
2198
|
const dy = l.sY - screenH / 2;
|
|
1558
2199
|
rot = Math.atan2(-dy, -dx) - Math.PI / 2;
|
|
1559
2200
|
}
|
|
1560
|
-
l.uniforms.uAngle.value =
|
|
2201
|
+
l.uniforms.uAngle.value = THREE5__namespace.MathUtils.lerp(l.uniforms.uAngle.value, rot, 0.1);
|
|
1561
2202
|
}
|
|
1562
2203
|
if (l.level === 2) {
|
|
1563
|
-
|
|
1564
|
-
|
|
2204
|
+
if (showBooks || isSpecial) {
|
|
2205
|
+
target2 = 1;
|
|
2206
|
+
occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
|
|
2207
|
+
}
|
|
1565
2208
|
} else if (l.level === 1) {
|
|
1566
2209
|
if (showDivisions || isSpecial) {
|
|
1567
2210
|
const pad = -5;
|
|
@@ -1570,12 +2213,17 @@ function createEngine({
|
|
|
1570
2213
|
occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
|
|
1571
2214
|
}
|
|
1572
2215
|
}
|
|
1573
|
-
} else if (l.level === 3) {
|
|
2216
|
+
} else if (l.level === 2.5 || l.level === 3) {
|
|
1574
2217
|
if (showChapters || isSpecial) {
|
|
1575
2218
|
target2 = 1;
|
|
2219
|
+
if (!isSpecial) {
|
|
2220
|
+
const dist = Math.sqrt(l.ndcX * l.ndcX + l.ndcY * l.ndcY);
|
|
2221
|
+
const focusFade = 1 - THREE5__namespace.MathUtils.smoothstep(0.4, 0.7, dist);
|
|
2222
|
+
target2 *= focusFade;
|
|
2223
|
+
}
|
|
1576
2224
|
}
|
|
1577
2225
|
}
|
|
1578
|
-
l.uniforms.uAlpha.value =
|
|
2226
|
+
l.uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(l.uniforms.uAlpha.value, target2, 0.1);
|
|
1579
2227
|
l.item.obj.visible = l.uniforms.uAlpha.value > 0.01;
|
|
1580
2228
|
}
|
|
1581
2229
|
renderer.render(scene, camera);
|
|
@@ -1592,16 +2240,31 @@ function createEngine({
|
|
|
1592
2240
|
}
|
|
1593
2241
|
function dispose() {
|
|
1594
2242
|
stop();
|
|
2243
|
+
constellationLayer.dispose();
|
|
1595
2244
|
renderer.dispose();
|
|
1596
2245
|
renderer.domElement.remove();
|
|
1597
2246
|
}
|
|
1598
|
-
|
|
2247
|
+
function setHoveredBook(id) {
|
|
2248
|
+
if (id === hoveredBookId) return;
|
|
2249
|
+
if (hoveredBookId) {
|
|
2250
|
+
hoverCooldowns.set(hoveredBookId, performance.now());
|
|
2251
|
+
}
|
|
2252
|
+
hoveredBookId = id;
|
|
2253
|
+
}
|
|
2254
|
+
function setFocusedBook(id) {
|
|
2255
|
+
focusedBookId = id;
|
|
2256
|
+
}
|
|
2257
|
+
function setOrderRevealEnabled(enabled) {
|
|
2258
|
+
orderRevealEnabled = enabled;
|
|
2259
|
+
}
|
|
2260
|
+
return { setConfig, start, stop, dispose, setHandlers, getFullArrangement, setHoveredBook, setFocusedBook, setOrderRevealEnabled };
|
|
1599
2261
|
}
|
|
1600
|
-
var ENGINE_CONFIG;
|
|
2262
|
+
var ENGINE_CONFIG, ORDER_REVEAL_CONFIG;
|
|
1601
2263
|
var init_createEngine = __esm({
|
|
1602
2264
|
"src/engine/createEngine.ts"() {
|
|
1603
2265
|
init_layout();
|
|
1604
2266
|
init_materials();
|
|
2267
|
+
init_ConstellationArtworkLayer();
|
|
1605
2268
|
ENGINE_CONFIG = {
|
|
1606
2269
|
minFov: 10,
|
|
1607
2270
|
maxFov: 165,
|
|
@@ -1614,16 +2277,26 @@ var init_createEngine = __esm({
|
|
|
1614
2277
|
zenithStrength: 0.02,
|
|
1615
2278
|
horizonLockStrength: 0.05,
|
|
1616
2279
|
edgePanThreshold: 0.15,
|
|
1617
|
-
edgePanMaxSpeed: 0.02
|
|
2280
|
+
edgePanMaxSpeed: 0.02,
|
|
2281
|
+
edgePanDelay: 250
|
|
2282
|
+
};
|
|
2283
|
+
ORDER_REVEAL_CONFIG = {
|
|
2284
|
+
globalDim: 0.85,
|
|
2285
|
+
pulseAmplitude: 0.6,
|
|
2286
|
+
pulseDuration: 2,
|
|
2287
|
+
delayPerChapter: 0.1
|
|
1618
2288
|
};
|
|
1619
2289
|
}
|
|
1620
2290
|
});
|
|
1621
2291
|
var StarMap = react.forwardRef(
|
|
1622
|
-
({ config, className, onSelect, onHover, onArrangementChange }, ref) => {
|
|
2292
|
+
({ config, className, onSelect, onHover, onArrangementChange, onFovChange }, ref) => {
|
|
1623
2293
|
const containerRef = react.useRef(null);
|
|
1624
2294
|
const engineRef = react.useRef(null);
|
|
1625
2295
|
react.useImperativeHandle(ref, () => ({
|
|
1626
|
-
getFullArrangement: () => engineRef.current?.getFullArrangement?.()
|
|
2296
|
+
getFullArrangement: () => engineRef.current?.getFullArrangement?.(),
|
|
2297
|
+
setHoveredBook: (id) => engineRef.current?.setHoveredBook?.(id),
|
|
2298
|
+
setFocusedBook: (id) => engineRef.current?.setFocusedBook?.(id),
|
|
2299
|
+
setOrderRevealEnabled: (enabled) => engineRef.current?.setOrderRevealEnabled?.(enabled)
|
|
1627
2300
|
}));
|
|
1628
2301
|
react.useEffect(() => {
|
|
1629
2302
|
let disposed = false;
|
|
@@ -1635,7 +2308,8 @@ var StarMap = react.forwardRef(
|
|
|
1635
2308
|
container: containerRef.current,
|
|
1636
2309
|
onSelect,
|
|
1637
2310
|
onHover,
|
|
1638
|
-
onArrangementChange
|
|
2311
|
+
onArrangementChange,
|
|
2312
|
+
onFovChange
|
|
1639
2313
|
});
|
|
1640
2314
|
engineRef.current.setConfig(config);
|
|
1641
2315
|
engineRef.current.start();
|
|
@@ -1651,8 +2325,8 @@ var StarMap = react.forwardRef(
|
|
|
1651
2325
|
engineRef.current?.setConfig?.(config);
|
|
1652
2326
|
}, [config]);
|
|
1653
2327
|
react.useEffect(() => {
|
|
1654
|
-
engineRef.current?.setHandlers?.({ onSelect, onHover, onArrangementChange });
|
|
1655
|
-
}, [onSelect, onHover, onArrangementChange]);
|
|
2328
|
+
engineRef.current?.setHandlers?.({ onSelect, onHover, onArrangementChange, onFovChange });
|
|
2329
|
+
}, [onSelect, onHover, onArrangementChange, onFovChange]);
|
|
1656
2330
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className, style: { width: "100%", height: "100%" } });
|
|
1657
2331
|
}
|
|
1658
2332
|
);
|
|
@@ -1661,7 +2335,6 @@ var StarMap = react.forwardRef(
|
|
|
1661
2335
|
function bibleToSceneModel(data) {
|
|
1662
2336
|
const nodes = [];
|
|
1663
2337
|
const links = [];
|
|
1664
|
-
let bookCounter = 0;
|
|
1665
2338
|
const id = {
|
|
1666
2339
|
testament: (t) => `T:${t}`,
|
|
1667
2340
|
division: (t, d) => `D:${t}:${d}`,
|
|
@@ -1682,8 +2355,7 @@ function bibleToSceneModel(data) {
|
|
|
1682
2355
|
});
|
|
1683
2356
|
links.push({ source: did, target: tid });
|
|
1684
2357
|
for (const b of d.books) {
|
|
1685
|
-
|
|
1686
|
-
const bookLabel = `${bookCounter}. ${b.name}`;
|
|
2358
|
+
const bookLabel = b.name;
|
|
1687
2359
|
const bid = id.book(b.key);
|
|
1688
2360
|
nodes.push({
|
|
1689
2361
|
id: bid,
|
|
@@ -30796,7 +31468,7 @@ var RNG = class {
|
|
|
30796
31468
|
const r = Math.sqrt(1 - y * y);
|
|
30797
31469
|
const x = r * Math.cos(theta);
|
|
30798
31470
|
const z = r * Math.sin(theta);
|
|
30799
|
-
return new
|
|
31471
|
+
return new THREE5__namespace.Vector3(x, y, z);
|
|
30800
31472
|
}
|
|
30801
31473
|
};
|
|
30802
31474
|
function simpleNoise3D(v, scale) {
|
|
@@ -30834,11 +31506,11 @@ function generateArrangement(bible, options = {}) {
|
|
|
30834
31506
|
});
|
|
30835
31507
|
});
|
|
30836
31508
|
const bookCount = books.length;
|
|
30837
|
-
const mwRad =
|
|
30838
|
-
const mwNormal = new
|
|
31509
|
+
const mwRad = THREE5__namespace.MathUtils.degToRad(opts.milkyWayAngle);
|
|
31510
|
+
const mwNormal = new THREE5__namespace.Vector3(Math.sin(mwRad), Math.cos(mwRad), 0).normalize();
|
|
30839
31511
|
const anchors = [];
|
|
30840
31512
|
for (let i = 0; i < bookCount; i++) {
|
|
30841
|
-
let bestP = new
|
|
31513
|
+
let bestP = new THREE5__namespace.Vector3();
|
|
30842
31514
|
let valid = false;
|
|
30843
31515
|
let attempt = 0;
|
|
30844
31516
|
while (!valid && attempt < 100) {
|
|
@@ -30864,7 +31536,7 @@ function generateArrangement(bible, options = {}) {
|
|
|
30864
31536
|
arrangement[`B:${book.key}`] = { position: [anchorPos.x, anchorPos.y, anchorPos.z] };
|
|
30865
31537
|
for (let c = 0; c < book.chapters; c++) {
|
|
30866
31538
|
const localSpread = opts.clusterSpread * (0.8 + rng.next() * 0.4);
|
|
30867
|
-
const offset = new
|
|
31539
|
+
const offset = new THREE5__namespace.Vector3(
|
|
30868
31540
|
(rng.next() - 0.5) * 2,
|
|
30869
31541
|
(rng.next() - 0.5) * 2,
|
|
30870
31542
|
(rng.next() - 0.5) * 2
|
|
@@ -30885,7 +31557,7 @@ function generateArrangement(bible, options = {}) {
|
|
|
30885
31557
|
const anchorPos = anchor.clone().multiplyScalar(opts.discRadius);
|
|
30886
31558
|
const divId = `D:${book.testament}:${book.division}`;
|
|
30887
31559
|
if (!divisions.has(divId)) {
|
|
30888
|
-
divisions.set(divId, { sum: new
|
|
31560
|
+
divisions.set(divId, { sum: new THREE5__namespace.Vector3(), count: 0 });
|
|
30889
31561
|
}
|
|
30890
31562
|
const entry = divisions.get(divId);
|
|
30891
31563
|
entry.sum.add(anchorPos);
|