@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.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 = {
|
|
@@ -446,12 +714,15 @@ function createEngine({
|
|
|
446
714
|
draggedNodeId: null,
|
|
447
715
|
draggedStarIndex: -1,
|
|
448
716
|
draggedDist: 2e3,
|
|
449
|
-
draggedGroup: null
|
|
717
|
+
draggedGroup: null,
|
|
718
|
+
tempArrangement: {}
|
|
450
719
|
};
|
|
451
|
-
const mouseNDC = new
|
|
720
|
+
const mouseNDC = new THREE5__namespace.Vector2();
|
|
452
721
|
let isMouseInWindow = false;
|
|
453
|
-
let
|
|
722
|
+
let edgeHoverStart = 0;
|
|
723
|
+
let handlers = { onSelect, onHover, onArrangementChange, onFovChange };
|
|
454
724
|
let currentConfig;
|
|
725
|
+
const constellationLayer = new ConstellationArtworkLayer(scene);
|
|
455
726
|
function mix(a, b, t) {
|
|
456
727
|
return a * (1 - t) + b * t;
|
|
457
728
|
}
|
|
@@ -486,7 +757,7 @@ function createEngine({
|
|
|
486
757
|
const phi = Math.atan2(uvY, uvX);
|
|
487
758
|
const sinTheta = Math.sin(theta);
|
|
488
759
|
const cosTheta = Math.cos(theta);
|
|
489
|
-
return new
|
|
760
|
+
return new THREE5__namespace.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
|
|
490
761
|
}
|
|
491
762
|
function getMouseWorldVector(pixelX, pixelY, width, height) {
|
|
492
763
|
const aspect = width / height;
|
|
@@ -505,7 +776,7 @@ function createEngine({
|
|
|
505
776
|
const phi = Math.atan2(uvY, uvX);
|
|
506
777
|
const sinTheta = Math.sin(theta);
|
|
507
778
|
const cosTheta = Math.cos(theta);
|
|
508
|
-
const vView = new
|
|
779
|
+
const vView = new THREE5__namespace.Vector3(sinTheta * Math.cos(phi), sinTheta * Math.sin(phi), -cosTheta).normalize();
|
|
509
780
|
return vView.applyQuaternion(camera.quaternion);
|
|
510
781
|
}
|
|
511
782
|
function smartProjectJS(worldPos) {
|
|
@@ -518,147 +789,187 @@ function createEngine({
|
|
|
518
789
|
const k = mix(kLinear, kStereo, blend);
|
|
519
790
|
return { x: k * dir.x, y: k * dir.y, z: dir.z };
|
|
520
791
|
}
|
|
521
|
-
const groundGroup = new
|
|
792
|
+
const groundGroup = new THREE5__namespace.Group();
|
|
522
793
|
scene.add(groundGroup);
|
|
523
794
|
function createGround() {
|
|
524
795
|
groundGroup.clear();
|
|
525
796
|
const radius = 995;
|
|
526
|
-
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);
|
|
527
798
|
const material = createSmartMaterial({
|
|
528
|
-
uniforms: {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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,
|
|
532
851
|
transparent: false,
|
|
533
852
|
depthWrite: true,
|
|
534
853
|
depthTest: true
|
|
535
854
|
});
|
|
536
|
-
const ground = new
|
|
855
|
+
const ground = new THREE5__namespace.Mesh(geometry, material);
|
|
537
856
|
groundGroup.add(ground);
|
|
538
|
-
const boxGeo = new THREE4__namespace.BoxGeometry(8, 30, 8);
|
|
539
|
-
for (let i = 0; i < 12; i++) {
|
|
540
|
-
const angle = i / 12 * Math.PI * 2;
|
|
541
|
-
const b = new THREE4__namespace.Mesh(boxGeo, material);
|
|
542
|
-
const r = radius * 0.98;
|
|
543
|
-
b.position.set(Math.cos(angle) * r, -15, Math.sin(angle) * r);
|
|
544
|
-
b.lookAt(0, 0, 0);
|
|
545
|
-
groundGroup.add(b);
|
|
546
|
-
}
|
|
547
857
|
}
|
|
858
|
+
let atmosphereMesh = null;
|
|
548
859
|
function createAtmosphere() {
|
|
549
|
-
const geometry = new
|
|
860
|
+
const geometry = new THREE5__namespace.SphereGeometry(990, 64, 64);
|
|
550
861
|
const material = createSmartMaterial({
|
|
551
|
-
uniforms: { top: { value: new THREE4__namespace.Color(0) }, bot: { value: new THREE4__namespace.Color(1712172) } },
|
|
552
862
|
vertexShaderBody: `
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
vec4 mv = modelViewMatrix * vec4(position, 1.0);
|
|
561
|
-
|
|
562
|
-
gl_Position = smartProject(mv);
|
|
563
|
-
|
|
564
|
-
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
`,
|
|
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
|
+
}`,
|
|
569
870
|
fragmentShader: `
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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,
|
|
593
901
|
depthWrite: false,
|
|
594
902
|
depthTest: true
|
|
595
903
|
});
|
|
596
|
-
const atm = new
|
|
904
|
+
const atm = new THREE5__namespace.Mesh(geometry, material);
|
|
905
|
+
atmosphereMesh = atm;
|
|
597
906
|
groundGroup.add(atm);
|
|
598
907
|
}
|
|
599
|
-
const backdropGroup = new
|
|
908
|
+
const backdropGroup = new THREE5__namespace.Group();
|
|
600
909
|
scene.add(backdropGroup);
|
|
601
|
-
function createBackdropStars() {
|
|
910
|
+
function createBackdropStars(count = 31e3) {
|
|
602
911
|
backdropGroup.clear();
|
|
603
|
-
|
|
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();
|
|
604
919
|
const positions = [];
|
|
605
920
|
const sizes = [];
|
|
606
921
|
const colors = [];
|
|
607
|
-
const colorPalette = [
|
|
608
|
-
new THREE4__namespace.Color(10203391),
|
|
609
|
-
new THREE4__namespace.Color(11190271),
|
|
610
|
-
new THREE4__namespace.Color(13293567),
|
|
611
|
-
new THREE4__namespace.Color(16316415),
|
|
612
|
-
new THREE4__namespace.Color(16774378),
|
|
613
|
-
new THREE4__namespace.Color(16765601),
|
|
614
|
-
new THREE4__namespace.Color(16764015)
|
|
615
|
-
];
|
|
616
922
|
const r = 2500;
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
v.normalize();
|
|
626
|
-
v.applyAxisAngle(new THREE4__namespace.Vector3(1, 0, 0), THREE4__namespace.MathUtils.degToRad(60));
|
|
627
|
-
x = v.x * r;
|
|
628
|
-
y = v.y * r;
|
|
629
|
-
z = v.z * r;
|
|
630
|
-
} else {
|
|
631
|
-
const u = Math.random();
|
|
632
|
-
const v = Math.random();
|
|
633
|
-
const theta = 2 * Math.PI * u;
|
|
634
|
-
const phi = Math.acos(2 * v - 1);
|
|
635
|
-
x = r * Math.sin(phi) * Math.cos(theta);
|
|
636
|
-
y = r * Math.sin(phi) * Math.sin(theta);
|
|
637
|
-
z = r * Math.cos(phi);
|
|
638
|
-
}
|
|
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);
|
|
639
931
|
positions.push(x, y, z);
|
|
640
|
-
const size =
|
|
932
|
+
const size = 1 + -Math.log(Math.random()) * 0.8 * 1.5;
|
|
641
933
|
sizes.push(size);
|
|
642
|
-
|
|
643
|
-
const c = colorPalette[cIndex];
|
|
644
|
-
colors.push(c.r, c.g, c.b);
|
|
934
|
+
colors.push(1, 1, 1);
|
|
645
935
|
}
|
|
646
|
-
geometry.setAttribute("position", new
|
|
647
|
-
geometry.setAttribute("size", new
|
|
648
|
-
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));
|
|
649
939
|
const material = createSmartMaterial({
|
|
650
|
-
uniforms: {
|
|
940
|
+
uniforms: {
|
|
941
|
+
pixelRatio: { value: renderer.getPixelRatio() },
|
|
942
|
+
uScale: globalUniforms.uScale
|
|
943
|
+
},
|
|
651
944
|
vertexShaderBody: `
|
|
652
945
|
attribute float size;
|
|
653
946
|
attribute vec3 color;
|
|
654
947
|
varying vec3 vColor;
|
|
655
948
|
uniform float pixelRatio;
|
|
949
|
+
|
|
950
|
+
uniform float uAtmExtinction;
|
|
951
|
+
|
|
656
952
|
void main() {
|
|
657
|
-
|
|
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
|
+
|
|
658
964
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
659
965
|
gl_Position = smartProject(mvPosition);
|
|
660
966
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
661
|
-
|
|
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;
|
|
662
973
|
}
|
|
663
974
|
`,
|
|
664
975
|
fragmentShader: `
|
|
@@ -669,31 +980,81 @@ function createEngine({
|
|
|
669
980
|
if (dist > 1.0) discard;
|
|
670
981
|
float alphaMask = getMaskAlpha();
|
|
671
982
|
if (alphaMask < 0.01) discard;
|
|
672
|
-
|
|
673
|
-
|
|
983
|
+
|
|
984
|
+
// Sharp falloff for intense point look
|
|
985
|
+
float alpha = exp(-4.0 * dist * dist);
|
|
674
986
|
gl_FragColor = vec4(vColor, alpha * alphaMask);
|
|
675
987
|
}
|
|
676
988
|
`,
|
|
677
989
|
transparent: true,
|
|
678
990
|
depthWrite: false,
|
|
679
|
-
depthTest: true
|
|
991
|
+
depthTest: true,
|
|
992
|
+
blending: THREE5__namespace.AdditiveBlending
|
|
680
993
|
});
|
|
681
|
-
const points = new
|
|
994
|
+
const points = new THREE5__namespace.Points(geometry, material);
|
|
682
995
|
points.frustumCulled = false;
|
|
683
996
|
backdropGroup.add(points);
|
|
684
997
|
}
|
|
685
998
|
createGround();
|
|
686
999
|
createAtmosphere();
|
|
687
1000
|
createBackdropStars();
|
|
688
|
-
const raycaster = new
|
|
1001
|
+
const raycaster = new THREE5__namespace.Raycaster();
|
|
689
1002
|
raycaster.params.Points.threshold = 5;
|
|
690
|
-
new
|
|
691
|
-
const root = new
|
|
1003
|
+
new THREE5__namespace.Vector2();
|
|
1004
|
+
const root = new THREE5__namespace.Group();
|
|
692
1005
|
scene.add(root);
|
|
693
1006
|
const nodeById = /* @__PURE__ */ new Map();
|
|
694
1007
|
const starIndexToId = [];
|
|
695
1008
|
const dynamicLabels = [];
|
|
1009
|
+
const hoverLabelMat = createSmartMaterial({
|
|
1010
|
+
uniforms: {
|
|
1011
|
+
uMap: { value: null },
|
|
1012
|
+
uSize: { value: new THREE5__namespace.Vector2(1, 1) },
|
|
1013
|
+
uAlpha: { value: 0 },
|
|
1014
|
+
uAngle: { value: 0 }
|
|
1015
|
+
},
|
|
1016
|
+
vertexShaderBody: `
|
|
1017
|
+
uniform vec2 uSize;
|
|
1018
|
+
uniform float uAngle;
|
|
1019
|
+
varying vec2 vUv;
|
|
1020
|
+
void main() {
|
|
1021
|
+
vUv = uv;
|
|
1022
|
+
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
1023
|
+
vec4 projected = smartProject(mvPos);
|
|
1024
|
+
|
|
1025
|
+
float c = cos(uAngle);
|
|
1026
|
+
float s = sin(uAngle);
|
|
1027
|
+
mat2 rot = mat2(c, -s, s, c);
|
|
1028
|
+
vec2 offset = rot * (position.xy * uSize);
|
|
1029
|
+
|
|
1030
|
+
projected.xy += offset / vec2(uAspect, 1.0);
|
|
1031
|
+
gl_Position = projected;
|
|
1032
|
+
}
|
|
1033
|
+
`,
|
|
1034
|
+
fragmentShader: `
|
|
1035
|
+
uniform sampler2D uMap;
|
|
1036
|
+
uniform float uAlpha;
|
|
1037
|
+
varying vec2 vUv;
|
|
1038
|
+
void main() {
|
|
1039
|
+
float mask = getMaskAlpha();
|
|
1040
|
+
if (mask < 0.01) discard;
|
|
1041
|
+
vec4 tex = texture2D(uMap, vUv);
|
|
1042
|
+
gl_FragColor = vec4(tex.rgb, tex.a * uAlpha * mask);
|
|
1043
|
+
}
|
|
1044
|
+
`,
|
|
1045
|
+
transparent: true,
|
|
1046
|
+
depthWrite: false,
|
|
1047
|
+
depthTest: false
|
|
1048
|
+
// Always on top of stars
|
|
1049
|
+
});
|
|
1050
|
+
const hoverLabelMesh = new THREE5__namespace.Mesh(new THREE5__namespace.PlaneGeometry(1, 1), hoverLabelMat);
|
|
1051
|
+
hoverLabelMesh.visible = false;
|
|
1052
|
+
hoverLabelMesh.renderOrder = 999;
|
|
1053
|
+
hoverLabelMesh.frustumCulled = false;
|
|
1054
|
+
root.add(hoverLabelMesh);
|
|
1055
|
+
let currentHoverNodeId = null;
|
|
696
1056
|
let constellationLines = null;
|
|
1057
|
+
let boundaryLines = null;
|
|
697
1058
|
let starPoints = null;
|
|
698
1059
|
function clearRoot() {
|
|
699
1060
|
for (const child of [...root.children]) {
|
|
@@ -709,6 +1070,7 @@ function createEngine({
|
|
|
709
1070
|
starIndexToId.length = 0;
|
|
710
1071
|
dynamicLabels.length = 0;
|
|
711
1072
|
constellationLines = null;
|
|
1073
|
+
boundaryLines = null;
|
|
712
1074
|
starPoints = null;
|
|
713
1075
|
}
|
|
714
1076
|
function createTextTexture(text, color = "#ffffff") {
|
|
@@ -716,58 +1078,102 @@ function createEngine({
|
|
|
716
1078
|
const ctx = canvas.getContext("2d");
|
|
717
1079
|
if (!ctx) return null;
|
|
718
1080
|
const fontSize = 96;
|
|
719
|
-
|
|
1081
|
+
const font = `400 ${fontSize}px "Inter", system-ui, sans-serif`;
|
|
1082
|
+
ctx.font = font;
|
|
720
1083
|
const metrics = ctx.measureText(text);
|
|
721
1084
|
const w = Math.ceil(metrics.width);
|
|
722
1085
|
const h = Math.ceil(fontSize * 1.2);
|
|
723
1086
|
canvas.width = w;
|
|
724
1087
|
canvas.height = h;
|
|
725
|
-
ctx.font =
|
|
1088
|
+
ctx.font = font;
|
|
726
1089
|
ctx.fillStyle = color;
|
|
727
1090
|
ctx.textAlign = "center";
|
|
728
1091
|
ctx.textBaseline = "middle";
|
|
729
1092
|
ctx.fillText(text, w / 2, h / 2);
|
|
730
|
-
const tex = new
|
|
731
|
-
tex.minFilter =
|
|
1093
|
+
const tex = new THREE5__namespace.CanvasTexture(canvas);
|
|
1094
|
+
tex.minFilter = THREE5__namespace.LinearFilter;
|
|
732
1095
|
return { tex, aspect: w / h };
|
|
733
1096
|
}
|
|
734
1097
|
function getPosition(n) {
|
|
735
1098
|
if (currentConfig?.arrangement) {
|
|
736
1099
|
const arr = currentConfig.arrangement[n.id];
|
|
737
|
-
if (arr)
|
|
1100
|
+
if (arr) {
|
|
1101
|
+
if (arr.position[2] === 0) {
|
|
1102
|
+
const x = arr.position[0];
|
|
1103
|
+
const y = arr.position[1];
|
|
1104
|
+
const radius = currentConfig.layout?.radius ?? 2e3;
|
|
1105
|
+
const r_norm = Math.min(1, Math.sqrt(x * x + y * y) / radius);
|
|
1106
|
+
const phi = Math.atan2(y, x);
|
|
1107
|
+
const theta = r_norm * (Math.PI / 2);
|
|
1108
|
+
return new THREE5__namespace.Vector3(
|
|
1109
|
+
Math.sin(theta) * Math.cos(phi),
|
|
1110
|
+
Math.cos(theta),
|
|
1111
|
+
Math.sin(theta) * Math.sin(phi)
|
|
1112
|
+
).multiplyScalar(radius);
|
|
1113
|
+
}
|
|
1114
|
+
return new THREE5__namespace.Vector3(arr.position[0], arr.position[1], arr.position[2]);
|
|
1115
|
+
}
|
|
738
1116
|
}
|
|
739
|
-
return new
|
|
1117
|
+
return new THREE5__namespace.Vector3(n.meta?.x ?? 0, n.meta?.y ?? 0, n.meta?.z ?? 0);
|
|
740
1118
|
}
|
|
741
1119
|
function getBoundaryPoint(angle, t, radius) {
|
|
742
1120
|
const y = 0.05 + t * (1 - 0.05);
|
|
743
1121
|
const rY = Math.sqrt(1 - y * y);
|
|
744
1122
|
const x = Math.cos(angle) * rY;
|
|
745
1123
|
const z = Math.sin(angle) * rY;
|
|
746
|
-
return new
|
|
1124
|
+
return new THREE5__namespace.Vector3(x, y, z).multiplyScalar(radius);
|
|
747
1125
|
}
|
|
748
1126
|
function buildFromModel(model, cfg) {
|
|
749
1127
|
clearRoot();
|
|
750
|
-
|
|
1128
|
+
bookIdToIndex.clear();
|
|
1129
|
+
scene.background = cfg.background && cfg.background !== "transparent" ? new THREE5__namespace.Color(cfg.background) : new THREE5__namespace.Color(0);
|
|
751
1130
|
const layoutCfg = { ...cfg.layout, radius: cfg.layout?.radius ?? 2e3 };
|
|
752
1131
|
const laidOut = computeLayoutPositions(model, layoutCfg);
|
|
1132
|
+
const divisionPositions = /* @__PURE__ */ new Map();
|
|
1133
|
+
if (cfg.arrangement) {
|
|
1134
|
+
const divMap = /* @__PURE__ */ new Map();
|
|
1135
|
+
for (const n of laidOut.nodes) {
|
|
1136
|
+
if (n.level === 2 && n.parent) {
|
|
1137
|
+
const list = divMap.get(n.parent) ?? [];
|
|
1138
|
+
list.push(n);
|
|
1139
|
+
divMap.set(n.parent, list);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
for (const [divId, books] of divMap.entries()) {
|
|
1143
|
+
const centroid = new THREE5__namespace.Vector3();
|
|
1144
|
+
let count = 0;
|
|
1145
|
+
for (const b of books) {
|
|
1146
|
+
const p = getPosition(b);
|
|
1147
|
+
centroid.add(p);
|
|
1148
|
+
count++;
|
|
1149
|
+
}
|
|
1150
|
+
if (count > 0) {
|
|
1151
|
+
centroid.divideScalar(count);
|
|
1152
|
+
divisionPositions.set(divId, centroid);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
753
1156
|
const starPositions = [];
|
|
754
1157
|
const starSizes = [];
|
|
755
1158
|
const starColors = [];
|
|
1159
|
+
const starPhases = [];
|
|
1160
|
+
const starBookIndices = [];
|
|
1161
|
+
const starChapterIndices = [];
|
|
756
1162
|
const SPECTRAL_COLORS = [
|
|
757
|
-
new
|
|
758
|
-
// O -
|
|
759
|
-
new
|
|
760
|
-
// B -
|
|
761
|
-
new
|
|
762
|
-
// A - White
|
|
763
|
-
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),
|
|
764
1170
|
// F - White
|
|
765
|
-
new
|
|
766
|
-
// G -
|
|
767
|
-
new
|
|
768
|
-
// K -
|
|
769
|
-
new
|
|
770
|
-
// 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
|
|
771
1177
|
];
|
|
772
1178
|
let minWeight = Infinity;
|
|
773
1179
|
let maxWeight = -Infinity;
|
|
@@ -792,32 +1198,62 @@ function createEngine({
|
|
|
792
1198
|
let baseSize = 3.5;
|
|
793
1199
|
if (typeof n.weight === "number") {
|
|
794
1200
|
const t = (n.weight - minWeight) / (maxWeight - minWeight);
|
|
795
|
-
baseSize =
|
|
1201
|
+
baseSize = 0.1 + Math.pow(t, 0.5) * 11.9;
|
|
796
1202
|
}
|
|
797
1203
|
starSizes.push(baseSize);
|
|
798
1204
|
const colorIdx = Math.floor(Math.pow(Math.random(), 1.5) * SPECTRAL_COLORS.length);
|
|
799
1205
|
const c = SPECTRAL_COLORS[Math.min(colorIdx, SPECTRAL_COLORS.length - 1)];
|
|
800
1206
|
starColors.push(c.r, c.g, c.b);
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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);
|
|
1219
|
+
}
|
|
1220
|
+
if (n.level === 1 || n.level === 2 || n.level === 3) {
|
|
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);
|
|
804
1230
|
if (texRes) {
|
|
805
|
-
|
|
806
|
-
|
|
1231
|
+
let baseScale = 0.05;
|
|
1232
|
+
if (n.level === 1) baseScale = 0.08;
|
|
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);
|
|
807
1236
|
const mat = createSmartMaterial({
|
|
808
1237
|
uniforms: {
|
|
809
1238
|
uMap: { value: texRes.tex },
|
|
810
1239
|
uSize: { value: size },
|
|
811
|
-
uAlpha: { value: 0 }
|
|
1240
|
+
uAlpha: { value: 0 },
|
|
1241
|
+
uAngle: { value: 0 }
|
|
812
1242
|
},
|
|
813
1243
|
vertexShaderBody: `
|
|
814
1244
|
uniform vec2 uSize;
|
|
1245
|
+
uniform float uAngle;
|
|
815
1246
|
varying vec2 vUv;
|
|
816
1247
|
void main() {
|
|
817
1248
|
vUv = uv;
|
|
818
1249
|
vec4 mvPos = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
819
1250
|
vec4 projected = smartProject(mvPos);
|
|
820
|
-
|
|
1251
|
+
|
|
1252
|
+
float c = cos(uAngle);
|
|
1253
|
+
float s = sin(uAngle);
|
|
1254
|
+
mat2 rot = mat2(c, -s, s, c);
|
|
1255
|
+
vec2 offset = rot * (position.xy * uSize);
|
|
1256
|
+
|
|
821
1257
|
projected.xy += offset / vec2(uAspect, 1.0);
|
|
822
1258
|
gl_Position = projected;
|
|
823
1259
|
}
|
|
@@ -837,8 +1273,19 @@ function createEngine({
|
|
|
837
1273
|
depthWrite: false,
|
|
838
1274
|
depthTest: true
|
|
839
1275
|
});
|
|
840
|
-
const mesh = new
|
|
841
|
-
|
|
1276
|
+
const mesh = new THREE5__namespace.Mesh(new THREE5__namespace.PlaneGeometry(1, 1), mat);
|
|
1277
|
+
let p = getPosition(n);
|
|
1278
|
+
if (n.level === 1) {
|
|
1279
|
+
if (divisionPositions.has(n.id)) {
|
|
1280
|
+
p.copy(divisionPositions.get(n.id));
|
|
1281
|
+
}
|
|
1282
|
+
const r = layoutCfg.radius * 0.95;
|
|
1283
|
+
const angle = Math.atan2(p.z, p.x);
|
|
1284
|
+
p.set(r * Math.cos(angle), 150, r * Math.sin(angle));
|
|
1285
|
+
} else if (n.level === 3) {
|
|
1286
|
+
p.y += 30;
|
|
1287
|
+
p.multiplyScalar(1.001);
|
|
1288
|
+
}
|
|
842
1289
|
mesh.position.set(p.x, p.y, p.z);
|
|
843
1290
|
mesh.scale.set(size.x, size.y, 1);
|
|
844
1291
|
mesh.frustumCulled = false;
|
|
@@ -848,47 +1295,119 @@ function createEngine({
|
|
|
848
1295
|
}
|
|
849
1296
|
}
|
|
850
1297
|
}
|
|
851
|
-
const starGeo = new
|
|
852
|
-
starGeo.setAttribute("position", new
|
|
853
|
-
starGeo.setAttribute("size", new
|
|
854
|
-
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));
|
|
855
1305
|
const starMat = createSmartMaterial({
|
|
856
|
-
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
|
+
},
|
|
857
1319
|
vertexShaderBody: `
|
|
858
1320
|
attribute float size;
|
|
859
1321
|
attribute vec3 color;
|
|
1322
|
+
attribute float phase;
|
|
1323
|
+
attribute float bookIndex;
|
|
1324
|
+
attribute float chapterIndex;
|
|
1325
|
+
|
|
860
1326
|
varying vec3 vColor;
|
|
861
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
|
+
|
|
862
1338
|
void main() {
|
|
863
|
-
|
|
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
|
+
|
|
864
1376
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
865
1377
|
gl_Position = smartProject(mvPosition);
|
|
866
1378
|
vScreenPos = gl_Position.xy / gl_Position.w;
|
|
867
|
-
|
|
1379
|
+
|
|
1380
|
+
float sizeBoost = 1.0 + activePulse * 0.8;
|
|
1381
|
+
gl_PointSize = (size * sizeBoost * 1.5) * uScale * pixelRatio * (2000.0 / -mvPosition.z) * horizonFade;
|
|
868
1382
|
}
|
|
869
1383
|
`,
|
|
870
1384
|
fragmentShader: `
|
|
871
1385
|
varying vec3 vColor;
|
|
872
1386
|
void main() {
|
|
873
1387
|
vec2 coord = gl_PointCoord - vec2(0.5);
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
if (dist > 1.0) discard;
|
|
1388
|
+
float d = length(coord) * 2.0;
|
|
1389
|
+
if (d > 1.0) discard;
|
|
877
1390
|
|
|
878
1391
|
float alphaMask = getMaskAlpha();
|
|
879
1392
|
if (alphaMask < 0.01) discard;
|
|
880
1393
|
|
|
881
|
-
|
|
882
|
-
|
|
1394
|
+
float dd = d * d;
|
|
1395
|
+
// Stellarium Profile
|
|
1396
|
+
float core = exp(-20.0 * dd);
|
|
1397
|
+
float halo = exp(-4.0 * dd);
|
|
883
1398
|
|
|
884
|
-
|
|
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);
|
|
885
1403
|
}
|
|
886
1404
|
`,
|
|
887
1405
|
transparent: true,
|
|
888
1406
|
depthWrite: false,
|
|
889
|
-
depthTest: true
|
|
1407
|
+
depthTest: true,
|
|
1408
|
+
blending: THREE5__namespace.AdditiveBlending
|
|
890
1409
|
});
|
|
891
|
-
starPoints = new
|
|
1410
|
+
starPoints = new THREE5__namespace.Points(starGeo, starMat);
|
|
892
1411
|
starPoints.frustumCulled = false;
|
|
893
1412
|
root.add(starPoints);
|
|
894
1413
|
const linePoints = [];
|
|
@@ -914,31 +1433,119 @@ function createEngine({
|
|
|
914
1433
|
}
|
|
915
1434
|
}
|
|
916
1435
|
if (linePoints.length > 0) {
|
|
917
|
-
const lineGeo = new
|
|
918
|
-
lineGeo.setAttribute("position", new
|
|
1436
|
+
const lineGeo = new THREE5__namespace.BufferGeometry();
|
|
1437
|
+
lineGeo.setAttribute("position", new THREE5__namespace.Float32BufferAttribute(linePoints, 3));
|
|
919
1438
|
const lineMat = createSmartMaterial({
|
|
920
|
-
uniforms: { color: { value: new
|
|
1439
|
+
uniforms: { color: { value: new THREE5__namespace.Color(11193599) } },
|
|
921
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; }`,
|
|
922
|
-
fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.
|
|
1441
|
+
fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.4 * alphaMask); }`,
|
|
923
1442
|
transparent: true,
|
|
924
1443
|
depthWrite: false,
|
|
925
|
-
blending:
|
|
1444
|
+
blending: THREE5__namespace.AdditiveBlending
|
|
926
1445
|
});
|
|
927
|
-
constellationLines = new
|
|
1446
|
+
constellationLines = new THREE5__namespace.LineSegments(lineGeo, lineMat);
|
|
928
1447
|
constellationLines.frustumCulled = false;
|
|
929
1448
|
root.add(constellationLines);
|
|
930
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
|
+
}
|
|
931
1538
|
const boundaries = laidOut.meta?.divisionBoundaries ?? [];
|
|
932
1539
|
if (boundaries.length > 0) {
|
|
933
1540
|
const boundaryMat = createSmartMaterial({
|
|
934
|
-
uniforms: { color: { value: new
|
|
1541
|
+
uniforms: { color: { value: new THREE5__namespace.Color(5601177) } },
|
|
935
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; }`,
|
|
936
1543
|
fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.10 * alphaMask); }`,
|
|
937
1544
|
transparent: true,
|
|
938
1545
|
depthWrite: false,
|
|
939
|
-
blending:
|
|
1546
|
+
blending: THREE5__namespace.AdditiveBlending
|
|
940
1547
|
});
|
|
941
|
-
const boundaryGeo = new
|
|
1548
|
+
const boundaryGeo = new THREE5__namespace.BufferGeometry();
|
|
942
1549
|
const bPoints = [];
|
|
943
1550
|
boundaries.forEach((angle) => {
|
|
944
1551
|
const steps = 32;
|
|
@@ -951,18 +1558,80 @@ function createEngine({
|
|
|
951
1558
|
bPoints.push(p2.x, p2.y, p2.z);
|
|
952
1559
|
}
|
|
953
1560
|
});
|
|
954
|
-
boundaryGeo.setAttribute("position", new
|
|
955
|
-
|
|
1561
|
+
boundaryGeo.setAttribute("position", new THREE5__namespace.Float32BufferAttribute(bPoints, 3));
|
|
1562
|
+
boundaryLines = new THREE5__namespace.LineSegments(boundaryGeo, boundaryMat);
|
|
956
1563
|
boundaryLines.frustumCulled = false;
|
|
957
1564
|
root.add(boundaryLines);
|
|
958
1565
|
}
|
|
1566
|
+
if (cfg.polygons) {
|
|
1567
|
+
const polyPoints = [];
|
|
1568
|
+
const rBase = layoutCfg.radius;
|
|
1569
|
+
for (const pts of Object.values(cfg.polygons)) {
|
|
1570
|
+
if (pts.length < 2) continue;
|
|
1571
|
+
for (let i = 0; i < pts.length; i++) {
|
|
1572
|
+
const p1_2d = pts[i];
|
|
1573
|
+
const p2_2d = pts[(i + 1) % pts.length];
|
|
1574
|
+
if (!p1_2d || !p2_2d) continue;
|
|
1575
|
+
const project2dTo3d = (p) => {
|
|
1576
|
+
const x = p[0];
|
|
1577
|
+
const y = p[1];
|
|
1578
|
+
const r_norm = Math.sqrt(x * x + y * y);
|
|
1579
|
+
const phi = Math.atan2(y, x);
|
|
1580
|
+
const theta = r_norm * (Math.PI / 2);
|
|
1581
|
+
return new THREE5__namespace.Vector3(
|
|
1582
|
+
Math.sin(theta) * Math.cos(phi),
|
|
1583
|
+
Math.cos(theta),
|
|
1584
|
+
Math.sin(theta) * Math.sin(phi)
|
|
1585
|
+
).multiplyScalar(rBase);
|
|
1586
|
+
};
|
|
1587
|
+
const v1 = project2dTo3d(p1_2d);
|
|
1588
|
+
const v2 = project2dTo3d(p2_2d);
|
|
1589
|
+
polyPoints.push(v1.x, v1.y, v1.z);
|
|
1590
|
+
polyPoints.push(v2.x, v2.y, v2.z);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
if (polyPoints.length > 0) {
|
|
1594
|
+
const polyGeo = new THREE5__namespace.BufferGeometry();
|
|
1595
|
+
polyGeo.setAttribute("position", new THREE5__namespace.Float32BufferAttribute(polyPoints, 3));
|
|
1596
|
+
const polyMat = createSmartMaterial({
|
|
1597
|
+
uniforms: { color: { value: new THREE5__namespace.Color(3718648) } },
|
|
1598
|
+
// Cyan-ish
|
|
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; }`,
|
|
1600
|
+
fragmentShader: `varying vec3 vColor; void main() { float alphaMask = getMaskAlpha(); if (alphaMask < 0.01) discard; gl_FragColor = vec4(vColor, 0.2 * alphaMask); }`,
|
|
1601
|
+
transparent: true,
|
|
1602
|
+
depthWrite: false,
|
|
1603
|
+
blending: THREE5__namespace.AdditiveBlending
|
|
1604
|
+
});
|
|
1605
|
+
const polyLines = new THREE5__namespace.LineSegments(polyGeo, polyMat);
|
|
1606
|
+
polyLines.frustumCulled = false;
|
|
1607
|
+
root.add(polyLines);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
959
1610
|
resize();
|
|
960
1611
|
}
|
|
961
1612
|
let lastData = void 0;
|
|
962
1613
|
let lastAdapter = void 0;
|
|
963
1614
|
let lastModel = void 0;
|
|
1615
|
+
let lastAppliedLon = void 0;
|
|
1616
|
+
let lastAppliedLat = void 0;
|
|
1617
|
+
let lastBackdropCount = void 0;
|
|
964
1618
|
function setConfig(cfg) {
|
|
965
1619
|
currentConfig = cfg;
|
|
1620
|
+
if (typeof cfg.camera?.lon === "number" && cfg.camera.lon !== lastAppliedLon) {
|
|
1621
|
+
state.lon = cfg.camera.lon;
|
|
1622
|
+
state.targetLon = cfg.camera.lon;
|
|
1623
|
+
lastAppliedLon = cfg.camera.lon;
|
|
1624
|
+
}
|
|
1625
|
+
if (typeof cfg.camera?.lat === "number" && cfg.camera.lat !== lastAppliedLat) {
|
|
1626
|
+
state.lat = cfg.camera.lat;
|
|
1627
|
+
state.targetLat = cfg.camera.lat;
|
|
1628
|
+
lastAppliedLat = cfg.camera.lat;
|
|
1629
|
+
}
|
|
1630
|
+
const desiredBackdropCount = typeof cfg.backdropStarsCount === "number" ? cfg.backdropStarsCount : 4e3;
|
|
1631
|
+
if (lastBackdropCount !== desiredBackdropCount) {
|
|
1632
|
+
createBackdropStars(desiredBackdropCount);
|
|
1633
|
+
lastBackdropCount = desiredBackdropCount;
|
|
1634
|
+
}
|
|
966
1635
|
let shouldRebuild = false;
|
|
967
1636
|
let model = cfg.model;
|
|
968
1637
|
if (!model && cfg.data && cfg.adapter) {
|
|
@@ -986,6 +1655,29 @@ function createEngine({
|
|
|
986
1655
|
} else if (cfg.arrangement && starPoints) {
|
|
987
1656
|
if (lastModel) buildFromModel(lastModel, cfg);
|
|
988
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
|
+
}
|
|
989
1681
|
}
|
|
990
1682
|
function setHandlers(next) {
|
|
991
1683
|
handlers = next;
|
|
@@ -993,22 +1685,25 @@ function createEngine({
|
|
|
993
1685
|
function getFullArrangement() {
|
|
994
1686
|
const arr = {};
|
|
995
1687
|
if (starPoints && starPoints.geometry.attributes.position) {
|
|
996
|
-
const
|
|
1688
|
+
const attr = starPoints.geometry.attributes.position;
|
|
997
1689
|
for (let i = 0; i < starIndexToId.length; i++) {
|
|
998
1690
|
const id = starIndexToId[i];
|
|
999
1691
|
if (id) {
|
|
1000
|
-
const x =
|
|
1001
|
-
const y =
|
|
1002
|
-
const z =
|
|
1003
|
-
|
|
1004
|
-
arr[id] = { position: [x, y, z] };
|
|
1005
|
-
}
|
|
1692
|
+
const x = attr.getX(i);
|
|
1693
|
+
const y = attr.getY(i);
|
|
1694
|
+
const z = attr.getZ(i);
|
|
1695
|
+
arr[id] = { position: [x, y, z] };
|
|
1006
1696
|
}
|
|
1007
1697
|
}
|
|
1008
1698
|
}
|
|
1009
1699
|
for (const item of dynamicLabels) {
|
|
1700
|
+
if (item.node.level === 3) continue;
|
|
1010
1701
|
arr[item.node.id] = { position: [item.obj.position.x, item.obj.position.y, item.obj.position.z] };
|
|
1011
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
|
+
}
|
|
1706
|
+
Object.assign(arr, state.tempArrangement);
|
|
1012
1707
|
return arr;
|
|
1013
1708
|
}
|
|
1014
1709
|
function pick(ev) {
|
|
@@ -1017,16 +1712,18 @@ function createEngine({
|
|
|
1017
1712
|
const mY = ev.clientY - rect.top;
|
|
1018
1713
|
mouseNDC.x = mX / rect.width * 2 - 1;
|
|
1019
1714
|
mouseNDC.y = -(mY / rect.height) * 2 + 1;
|
|
1020
|
-
let closestLabel = null;
|
|
1021
|
-
let minLabelDist = 40;
|
|
1022
1715
|
const uScale = globalUniforms.uScale.value;
|
|
1023
1716
|
const uAspect = camera.aspect;
|
|
1024
1717
|
const w = rect.width;
|
|
1025
1718
|
const h = rect.height;
|
|
1719
|
+
let closestLabel = null;
|
|
1720
|
+
let minLabelDist = 40;
|
|
1026
1721
|
for (const item of dynamicLabels) {
|
|
1027
1722
|
if (!item.obj.visible) continue;
|
|
1028
1723
|
const pWorld = item.obj.position;
|
|
1029
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;
|
|
1030
1727
|
const xNDC = pProj.x * uScale / uAspect;
|
|
1031
1728
|
const yNDC = pProj.y * uScale;
|
|
1032
1729
|
const sX = (xNDC * 0.5 + 0.5) * w;
|
|
@@ -1034,24 +1731,72 @@ function createEngine({
|
|
|
1034
1731
|
const dx = mX - sX;
|
|
1035
1732
|
const dy = mY - sY;
|
|
1036
1733
|
const d = Math.sqrt(dx * dx + dy * dy);
|
|
1037
|
-
|
|
1038
|
-
if (!isBehind && d < minLabelDist) {
|
|
1734
|
+
if (d < minLabelDist) {
|
|
1039
1735
|
minLabelDist = d;
|
|
1040
1736
|
closestLabel = item;
|
|
1041
1737
|
}
|
|
1042
1738
|
}
|
|
1043
|
-
if (closestLabel)
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
+
}
|
|
1055
1800
|
}
|
|
1056
1801
|
}
|
|
1057
1802
|
return void 0;
|
|
@@ -1079,21 +1824,25 @@ function createEngine({
|
|
|
1079
1824
|
if (starId) {
|
|
1080
1825
|
const starNode = nodeById.get(starId);
|
|
1081
1826
|
if (starNode && starNode.parent === bookId) {
|
|
1082
|
-
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]) });
|
|
1083
1828
|
}
|
|
1084
1829
|
}
|
|
1085
1830
|
}
|
|
1086
1831
|
}
|
|
1087
1832
|
state.draggedGroup = { labelInitialPos: hit.object.position.clone(), children };
|
|
1088
1833
|
state.draggedStarIndex = -1;
|
|
1834
|
+
} else if (hit.type === "constellation") {
|
|
1835
|
+
state.draggedGroup = null;
|
|
1836
|
+
state.draggedStarIndex = -1;
|
|
1089
1837
|
}
|
|
1090
|
-
return;
|
|
1091
1838
|
}
|
|
1839
|
+
return;
|
|
1092
1840
|
}
|
|
1093
1841
|
state.dragMode = "camera";
|
|
1094
1842
|
state.isDragging = true;
|
|
1095
1843
|
state.velocityX = 0;
|
|
1096
1844
|
state.velocityY = 0;
|
|
1845
|
+
state.tempArrangement = {};
|
|
1097
1846
|
document.body.style.cursor = "grabbing";
|
|
1098
1847
|
}
|
|
1099
1848
|
function onMouseMove(e) {
|
|
@@ -1114,16 +1863,29 @@ function createEngine({
|
|
|
1114
1863
|
} else if (state.draggedGroup && state.draggedNodeId) {
|
|
1115
1864
|
const group = state.draggedGroup;
|
|
1116
1865
|
const item = dynamicLabels.find((l) => l.node.id === state.draggedNodeId);
|
|
1117
|
-
if (item)
|
|
1866
|
+
if (item) {
|
|
1867
|
+
item.obj.position.copy(newPos);
|
|
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
|
+
}
|
|
1875
|
+
}
|
|
1118
1876
|
const vStart = group.labelInitialPos.clone().normalize();
|
|
1119
1877
|
const vEnd = newPos.clone().normalize();
|
|
1120
|
-
const q = new
|
|
1878
|
+
const q = new THREE5__namespace.Quaternion().setFromUnitVectors(vStart, vEnd);
|
|
1121
1879
|
if (starPoints && group.children.length > 0) {
|
|
1122
1880
|
const attr = starPoints.geometry.attributes.position;
|
|
1123
|
-
const tempVec = new
|
|
1881
|
+
const tempVec = new THREE5__namespace.Vector3();
|
|
1124
1882
|
for (const child of group.children) {
|
|
1125
1883
|
tempVec.copy(child.initialPos).applyQuaternion(q);
|
|
1126
1884
|
attr.setXYZ(child.index, tempVec.x, tempVec.y, tempVec.z);
|
|
1885
|
+
const id = starIndexToId[child.index];
|
|
1886
|
+
if (id) {
|
|
1887
|
+
state.tempArrangement[id] = { position: [tempVec.x, tempVec.y, tempVec.z] };
|
|
1888
|
+
}
|
|
1127
1889
|
}
|
|
1128
1890
|
attr.needsUpdate = true;
|
|
1129
1891
|
}
|
|
@@ -1143,9 +1905,30 @@ function createEngine({
|
|
|
1143
1905
|
state.lat = state.targetLat;
|
|
1144
1906
|
} else {
|
|
1145
1907
|
const hit = pick(e);
|
|
1908
|
+
if (hit && hit.type === "star") {
|
|
1909
|
+
if (currentHoverNodeId !== hit.node.id) {
|
|
1910
|
+
currentHoverNodeId = hit.node.id;
|
|
1911
|
+
const res = createTextTexture(hit.node.label, "#ffd700");
|
|
1912
|
+
if (res) {
|
|
1913
|
+
hoverLabelMat.uniforms.uMap.value = res.tex;
|
|
1914
|
+
const baseScale = 0.03;
|
|
1915
|
+
const size = new THREE5__namespace.Vector2(baseScale * res.aspect, baseScale);
|
|
1916
|
+
hoverLabelMat.uniforms.uSize.value = size;
|
|
1917
|
+
hoverLabelMesh.scale.set(size.x, size.y, 1);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
hoverLabelMesh.position.copy(hit.point);
|
|
1921
|
+
hoverLabelMat.uniforms.uAlpha.value = 1;
|
|
1922
|
+
hoverLabelMesh.visible = true;
|
|
1923
|
+
} else {
|
|
1924
|
+
currentHoverNodeId = null;
|
|
1925
|
+
hoverLabelMat.uniforms.uAlpha.value = 0;
|
|
1926
|
+
hoverLabelMesh.visible = false;
|
|
1927
|
+
}
|
|
1146
1928
|
if (hit?.node.id !== handlers._lastHoverId) {
|
|
1147
1929
|
handlers._lastHoverId = hit?.node.id;
|
|
1148
1930
|
handlers.onHover?.(hit?.node);
|
|
1931
|
+
constellationLayer.setHovered(hit?.node.id ?? null);
|
|
1149
1932
|
}
|
|
1150
1933
|
document.body.style.cursor = hit ? currentConfig?.editable ? "crosshair" : "pointer" : "default";
|
|
1151
1934
|
}
|
|
@@ -1165,7 +1948,14 @@ function createEngine({
|
|
|
1165
1948
|
document.body.style.cursor = "default";
|
|
1166
1949
|
} else {
|
|
1167
1950
|
const hit = pick(e);
|
|
1168
|
-
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
|
+
}
|
|
1169
1959
|
}
|
|
1170
1960
|
}
|
|
1171
1961
|
function onWheel(e) {
|
|
@@ -1176,25 +1966,26 @@ function createEngine({
|
|
|
1176
1966
|
const zoomSpeed = 1e-3 * state.fov;
|
|
1177
1967
|
state.fov += e.deltaY * zoomSpeed;
|
|
1178
1968
|
state.fov = Math.max(ENGINE_CONFIG.minFov, Math.min(ENGINE_CONFIG.maxFov, state.fov));
|
|
1969
|
+
handlers.onFovChange?.(state.fov);
|
|
1179
1970
|
updateUniforms();
|
|
1180
1971
|
const vAfter = getMouseViewVector(state.fov, aspect);
|
|
1181
|
-
const quaternion = new
|
|
1972
|
+
const quaternion = new THREE5__namespace.Quaternion().setFromUnitVectors(vAfter, vBefore);
|
|
1182
1973
|
const y = Math.sin(state.lat);
|
|
1183
1974
|
const r = Math.cos(state.lat);
|
|
1184
1975
|
const x = r * Math.sin(state.lon);
|
|
1185
1976
|
const z = -r * Math.cos(state.lon);
|
|
1186
|
-
const currentLook = new
|
|
1977
|
+
const currentLook = new THREE5__namespace.Vector3(x, y, z);
|
|
1187
1978
|
const camForward = currentLook.clone().normalize();
|
|
1188
1979
|
const camUp = camera.up.clone();
|
|
1189
|
-
const camRight = new
|
|
1190
|
-
const camUpOrtho = new
|
|
1191
|
-
const mat = new
|
|
1192
|
-
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);
|
|
1193
1984
|
const qNew = qOld.clone().multiply(quaternion);
|
|
1194
|
-
const newForward = new
|
|
1985
|
+
const newForward = new THREE5__namespace.Vector3(0, 0, -1).applyQuaternion(qNew);
|
|
1195
1986
|
state.lat = Math.asin(Math.max(-0.999, Math.min(0.999, newForward.y)));
|
|
1196
1987
|
state.lon = Math.atan2(newForward.x, -newForward.z);
|
|
1197
|
-
const newUp = new
|
|
1988
|
+
const newUp = new THREE5__namespace.Vector3(0, 1, 0).applyQuaternion(qNew);
|
|
1198
1989
|
camera.up.copy(newUp);
|
|
1199
1990
|
if (e.deltaY > 0 && state.fov > ENGINE_CONFIG.zenithStartFov) {
|
|
1200
1991
|
const range = ENGINE_CONFIG.maxFov - ENGINE_CONFIG.zenithStartFov;
|
|
@@ -1235,79 +2026,205 @@ function createEngine({
|
|
|
1235
2026
|
function tick() {
|
|
1236
2027
|
if (!running) return;
|
|
1237
2028
|
raf = requestAnimationFrame(tick);
|
|
1238
|
-
|
|
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) {
|
|
1239
2055
|
const t = ENGINE_CONFIG.edgePanThreshold;
|
|
1240
|
-
const
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
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
|
+
}
|
|
1262
2077
|
} else {
|
|
1263
|
-
|
|
1264
|
-
state.lat += state.velocityY;
|
|
1265
|
-
state.velocityX *= ENGINE_CONFIG.inertiaDamping;
|
|
1266
|
-
state.velocityY *= ENGINE_CONFIG.inertiaDamping;
|
|
1267
|
-
if (Math.abs(state.velocityX) < 1e-6) state.velocityX = 0;
|
|
1268
|
-
if (Math.abs(state.velocityY) < 1e-6) state.velocityY = 0;
|
|
2078
|
+
edgeHoverStart = 0;
|
|
1269
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;
|
|
1270
2088
|
} else if (!state.isDragging) {
|
|
1271
2089
|
state.lon += state.velocityX;
|
|
1272
2090
|
state.lat += state.velocityY;
|
|
1273
2091
|
state.velocityX *= ENGINE_CONFIG.inertiaDamping;
|
|
1274
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;
|
|
1275
2095
|
}
|
|
1276
2096
|
state.lat = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.lat));
|
|
1277
2097
|
const y = Math.sin(state.lat);
|
|
1278
2098
|
const r = Math.cos(state.lat);
|
|
1279
2099
|
const x = r * Math.sin(state.lon);
|
|
1280
2100
|
const z = -r * Math.cos(state.lon);
|
|
1281
|
-
const target = new
|
|
1282
|
-
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();
|
|
1283
2103
|
camera.up.lerp(idealUp, ENGINE_CONFIG.horizonLockStrength);
|
|
1284
2104
|
camera.up.normalize();
|
|
1285
2105
|
camera.lookAt(target);
|
|
2106
|
+
camera.updateMatrixWorld();
|
|
2107
|
+
camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
|
|
1286
2108
|
updateUniforms();
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
const
|
|
1291
|
-
const
|
|
2109
|
+
constellationLayer.update(state.fov, currentConfig?.showConstellationArt ?? false);
|
|
2110
|
+
backdropGroup.visible = currentConfig?.showBackdropStars ?? true;
|
|
2111
|
+
if (atmosphereMesh) atmosphereMesh.visible = currentConfig?.showAtmosphere ?? false;
|
|
2112
|
+
const DIVISION_THRESHOLD = 60;
|
|
2113
|
+
const showDivisions = state.fov > DIVISION_THRESHOLD;
|
|
2114
|
+
if (constellationLines) {
|
|
2115
|
+
constellationLines.visible = currentConfig?.showConstellationLines ?? false;
|
|
2116
|
+
}
|
|
2117
|
+
if (boundaryLines) {
|
|
2118
|
+
boundaryLines.visible = currentConfig?.showDivisionBoundaries ?? false;
|
|
2119
|
+
}
|
|
2120
|
+
const rect = renderer.domElement.getBoundingClientRect();
|
|
2121
|
+
const screenW = rect.width;
|
|
2122
|
+
const screenH = rect.height;
|
|
2123
|
+
const aspect = screenW / screenH;
|
|
2124
|
+
const labelsToCheck = [];
|
|
2125
|
+
const occupied = [];
|
|
2126
|
+
function isOverlapping(x2, y2, w, h) {
|
|
2127
|
+
for (const r2 of occupied) {
|
|
2128
|
+
if (x2 < r2.x + r2.w && x2 + w > r2.x && y2 < r2.y + r2.h && y2 + h > r2.y) return true;
|
|
2129
|
+
}
|
|
2130
|
+
return false;
|
|
2131
|
+
}
|
|
2132
|
+
const showBookLabels = currentConfig?.showBookLabels === true;
|
|
2133
|
+
const showDivisionLabels = currentConfig?.showDivisionLabels === true;
|
|
2134
|
+
const showChapterLabels = currentConfig?.showChapterLabels === true;
|
|
2135
|
+
const showGroupLabels = currentConfig?.showGroupLabels === true;
|
|
2136
|
+
const showBooks = state.fov < 120;
|
|
2137
|
+
const showChapters = state.fov < 70;
|
|
1292
2138
|
for (const item of dynamicLabels) {
|
|
1293
2139
|
const uniforms = item.obj.material.uniforms;
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
if (dot >= fullVisibleDot) gazeOpacity = 1;
|
|
1303
|
-
else if (dot > invisibleDot) gazeOpacity = (dot - invisibleDot) / (fullVisibleDot - invisibleDot);
|
|
1304
|
-
const zoomFactor = 1 - THREE4__namespace.MathUtils.smoothstep(40, SHOW_LABELS_FOV, state.fov);
|
|
1305
|
-
targetAlpha = gazeOpacity * zoomFactor;
|
|
1306
|
-
}
|
|
1307
|
-
if (uniforms.uAlpha) {
|
|
1308
|
-
uniforms.uAlpha.value = THREE4__namespace.MathUtils.lerp(uniforms.uAlpha.value, targetAlpha, 0.1);
|
|
2140
|
+
const level = item.node.level;
|
|
2141
|
+
let isEnabled = false;
|
|
2142
|
+
if (level === 2 && showBookLabels) isEnabled = true;
|
|
2143
|
+
else if (level === 1 && showDivisionLabels) isEnabled = true;
|
|
2144
|
+
else if (level === 3 && showChapterLabels) isEnabled = true;
|
|
2145
|
+
else if (level === 2.5 && showGroupLabels) isEnabled = true;
|
|
2146
|
+
if (!isEnabled) {
|
|
2147
|
+
uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
1309
2148
|
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
2149
|
+
continue;
|
|
1310
2150
|
}
|
|
2151
|
+
const pWorld = item.obj.position;
|
|
2152
|
+
const pProj = smartProjectJS(pWorld);
|
|
2153
|
+
if (pProj.z > 0.2) {
|
|
2154
|
+
uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(uniforms.uAlpha.value, 0, 0.2);
|
|
2155
|
+
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
2156
|
+
continue;
|
|
2157
|
+
}
|
|
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);
|
|
2165
|
+
item.obj.visible = uniforms.uAlpha.value > 0.01;
|
|
2166
|
+
continue;
|
|
2167
|
+
}
|
|
2168
|
+
const ndcX = pProj.x * globalUniforms.uScale.value / aspect;
|
|
2169
|
+
const ndcY = pProj.y * globalUniforms.uScale.value;
|
|
2170
|
+
const sX = (ndcX * 0.5 + 0.5) * screenW;
|
|
2171
|
+
const sY = (-ndcY * 0.5 + 0.5) * screenH;
|
|
2172
|
+
const size = uniforms.uSize.value;
|
|
2173
|
+
const pixelH = size.y * screenH * 0.8;
|
|
2174
|
+
const pixelW = size.x * screenH * 0.8;
|
|
2175
|
+
labelsToCheck.push({ item, sX, sY, w: pixelW, h: pixelH, uniforms, level, ndcX, ndcY });
|
|
2176
|
+
}
|
|
2177
|
+
const hoverId = handlers._lastHoverId;
|
|
2178
|
+
const selectedId = state.draggedNodeId;
|
|
2179
|
+
labelsToCheck.sort((a, b) => {
|
|
2180
|
+
const getScore = (l) => {
|
|
2181
|
+
if (l.item.node.id === selectedId) return 10;
|
|
2182
|
+
if (l.item.node.id === hoverId) return 9;
|
|
2183
|
+
const level = l.level;
|
|
2184
|
+
if (level === 2) return 5;
|
|
2185
|
+
if (level === 1) return showDivisions ? 6 : 1;
|
|
2186
|
+
return 0;
|
|
2187
|
+
};
|
|
2188
|
+
return getScore(b) - getScore(a);
|
|
2189
|
+
});
|
|
2190
|
+
for (const l of labelsToCheck) {
|
|
2191
|
+
let target2 = 0;
|
|
2192
|
+
const isSpecial = l.item.node.id === selectedId || l.item.node.id === hoverId;
|
|
2193
|
+
if (l.level === 1) {
|
|
2194
|
+
let rot = 0;
|
|
2195
|
+
const blend = globalUniforms.uBlend.value;
|
|
2196
|
+
if (blend > 0.5) {
|
|
2197
|
+
const dx = l.sX - screenW / 2;
|
|
2198
|
+
const dy = l.sY - screenH / 2;
|
|
2199
|
+
rot = Math.atan2(-dy, -dx) - Math.PI / 2;
|
|
2200
|
+
}
|
|
2201
|
+
l.uniforms.uAngle.value = THREE5__namespace.MathUtils.lerp(l.uniforms.uAngle.value, rot, 0.1);
|
|
2202
|
+
}
|
|
2203
|
+
if (l.level === 2) {
|
|
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
|
+
}
|
|
2208
|
+
} else if (l.level === 1) {
|
|
2209
|
+
if (showDivisions || isSpecial) {
|
|
2210
|
+
const pad = -5;
|
|
2211
|
+
if (!isOverlapping(l.sX - l.w / 2 - pad, l.sY - l.h / 2 - pad, l.w + pad * 2, l.h + pad * 2)) {
|
|
2212
|
+
target2 = 1;
|
|
2213
|
+
occupied.push({ x: l.sX - l.w / 2, y: l.sY - l.h / 2, w: l.w, h: l.h });
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
} else if (l.level === 2.5 || l.level === 3) {
|
|
2217
|
+
if (showChapters || isSpecial) {
|
|
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
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
l.uniforms.uAlpha.value = THREE5__namespace.MathUtils.lerp(l.uniforms.uAlpha.value, target2, 0.1);
|
|
2227
|
+
l.item.obj.visible = l.uniforms.uAlpha.value > 0.01;
|
|
1311
2228
|
}
|
|
1312
2229
|
renderer.render(scene, camera);
|
|
1313
2230
|
}
|
|
@@ -1323,16 +2240,31 @@ function createEngine({
|
|
|
1323
2240
|
}
|
|
1324
2241
|
function dispose() {
|
|
1325
2242
|
stop();
|
|
2243
|
+
constellationLayer.dispose();
|
|
1326
2244
|
renderer.dispose();
|
|
1327
2245
|
renderer.domElement.remove();
|
|
1328
2246
|
}
|
|
1329
|
-
|
|
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 };
|
|
1330
2261
|
}
|
|
1331
|
-
var ENGINE_CONFIG;
|
|
2262
|
+
var ENGINE_CONFIG, ORDER_REVEAL_CONFIG;
|
|
1332
2263
|
var init_createEngine = __esm({
|
|
1333
2264
|
"src/engine/createEngine.ts"() {
|
|
1334
2265
|
init_layout();
|
|
1335
2266
|
init_materials();
|
|
2267
|
+
init_ConstellationArtworkLayer();
|
|
1336
2268
|
ENGINE_CONFIG = {
|
|
1337
2269
|
minFov: 10,
|
|
1338
2270
|
maxFov: 165,
|
|
@@ -1345,16 +2277,26 @@ var init_createEngine = __esm({
|
|
|
1345
2277
|
zenithStrength: 0.02,
|
|
1346
2278
|
horizonLockStrength: 0.05,
|
|
1347
2279
|
edgePanThreshold: 0.15,
|
|
1348
|
-
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
|
|
1349
2288
|
};
|
|
1350
2289
|
}
|
|
1351
2290
|
});
|
|
1352
2291
|
var StarMap = react.forwardRef(
|
|
1353
|
-
({ config, className, onSelect, onHover, onArrangementChange }, ref) => {
|
|
2292
|
+
({ config, className, onSelect, onHover, onArrangementChange, onFovChange }, ref) => {
|
|
1354
2293
|
const containerRef = react.useRef(null);
|
|
1355
2294
|
const engineRef = react.useRef(null);
|
|
1356
2295
|
react.useImperativeHandle(ref, () => ({
|
|
1357
|
-
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)
|
|
1358
2300
|
}));
|
|
1359
2301
|
react.useEffect(() => {
|
|
1360
2302
|
let disposed = false;
|
|
@@ -1366,7 +2308,8 @@ var StarMap = react.forwardRef(
|
|
|
1366
2308
|
container: containerRef.current,
|
|
1367
2309
|
onSelect,
|
|
1368
2310
|
onHover,
|
|
1369
|
-
onArrangementChange
|
|
2311
|
+
onArrangementChange,
|
|
2312
|
+
onFovChange
|
|
1370
2313
|
});
|
|
1371
2314
|
engineRef.current.setConfig(config);
|
|
1372
2315
|
engineRef.current.start();
|
|
@@ -1382,8 +2325,8 @@ var StarMap = react.forwardRef(
|
|
|
1382
2325
|
engineRef.current?.setConfig?.(config);
|
|
1383
2326
|
}, [config]);
|
|
1384
2327
|
react.useEffect(() => {
|
|
1385
|
-
engineRef.current?.setHandlers?.({ onSelect, onHover, onArrangementChange });
|
|
1386
|
-
}, [onSelect, onHover, onArrangementChange]);
|
|
2328
|
+
engineRef.current?.setHandlers?.({ onSelect, onHover, onArrangementChange, onFovChange });
|
|
2329
|
+
}, [onSelect, onHover, onArrangementChange, onFovChange]);
|
|
1387
2330
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className, style: { width: "100%", height: "100%" } });
|
|
1388
2331
|
}
|
|
1389
2332
|
);
|
|
@@ -1412,10 +2355,11 @@ function bibleToSceneModel(data) {
|
|
|
1412
2355
|
});
|
|
1413
2356
|
links.push({ source: did, target: tid });
|
|
1414
2357
|
for (const b of d.books) {
|
|
2358
|
+
const bookLabel = b.name;
|
|
1415
2359
|
const bid = id.book(b.key);
|
|
1416
2360
|
nodes.push({
|
|
1417
2361
|
id: bid,
|
|
1418
|
-
label:
|
|
2362
|
+
label: bookLabel,
|
|
1419
2363
|
level: 2,
|
|
1420
2364
|
parent: did,
|
|
1421
2365
|
meta: { testament: t.name, division: d.name, bookKey: b.key, book: b.name }
|
|
@@ -1427,7 +2371,7 @@ function bibleToSceneModel(data) {
|
|
|
1427
2371
|
const cid = id.chapter(b.key, chapterNum);
|
|
1428
2372
|
nodes.push({
|
|
1429
2373
|
id: cid,
|
|
1430
|
-
label: `${
|
|
2374
|
+
label: `${bookLabel} ${chapterNum}`,
|
|
1431
2375
|
level: 3,
|
|
1432
2376
|
parent: bid,
|
|
1433
2377
|
weight: verseCounts[i],
|
|
@@ -30490,9 +31434,149 @@ var default_stars_default = {
|
|
|
30490
31434
|
}
|
|
30491
31435
|
]
|
|
30492
31436
|
};
|
|
31437
|
+
var defaultGenerateOptions = {
|
|
31438
|
+
seed: 12345,
|
|
31439
|
+
discRadius: 2e3,
|
|
31440
|
+
milkyWayEnabled: true,
|
|
31441
|
+
milkyWayAngle: 60,
|
|
31442
|
+
milkyWayWidth: 0.3,
|
|
31443
|
+
// Width in dot-product space
|
|
31444
|
+
milkyWayStrength: 0.7,
|
|
31445
|
+
noiseScale: 2,
|
|
31446
|
+
noiseStrength: 0.4,
|
|
31447
|
+
clusterSpread: 0.08
|
|
31448
|
+
// Radians approx
|
|
31449
|
+
};
|
|
31450
|
+
var RNG = class {
|
|
31451
|
+
seed;
|
|
31452
|
+
constructor(seed) {
|
|
31453
|
+
this.seed = seed;
|
|
31454
|
+
}
|
|
31455
|
+
// Returns 0..1
|
|
31456
|
+
next() {
|
|
31457
|
+
this.seed = (this.seed * 9301 + 49297) % 233280;
|
|
31458
|
+
return this.seed / 233280;
|
|
31459
|
+
}
|
|
31460
|
+
// Returns range [min, max)
|
|
31461
|
+
range(min, max) {
|
|
31462
|
+
return min + this.next() * (max - min);
|
|
31463
|
+
}
|
|
31464
|
+
// Uniform random on upper hemisphere (y > 0)
|
|
31465
|
+
randomOnSphere() {
|
|
31466
|
+
const y = this.next();
|
|
31467
|
+
const theta = 2 * Math.PI * this.next();
|
|
31468
|
+
const r = Math.sqrt(1 - y * y);
|
|
31469
|
+
const x = r * Math.cos(theta);
|
|
31470
|
+
const z = r * Math.sin(theta);
|
|
31471
|
+
return new THREE5__namespace.Vector3(x, y, z);
|
|
31472
|
+
}
|
|
31473
|
+
};
|
|
31474
|
+
function simpleNoise3D(v, scale) {
|
|
31475
|
+
const s = scale;
|
|
31476
|
+
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;
|
|
31477
|
+
}
|
|
31478
|
+
function getDensity(v, opts, mwNormal) {
|
|
31479
|
+
let density = 0.3;
|
|
31480
|
+
if (opts.milkyWayEnabled) {
|
|
31481
|
+
const dot = v.dot(mwNormal);
|
|
31482
|
+
const dist = Math.abs(dot);
|
|
31483
|
+
const band = Math.exp(-(dist * dist) / (opts.milkyWayWidth * opts.milkyWayWidth));
|
|
31484
|
+
density += band * opts.milkyWayStrength;
|
|
31485
|
+
}
|
|
31486
|
+
const noise = simpleNoise3D(v, opts.noiseScale);
|
|
31487
|
+
density *= 1 + noise * opts.noiseStrength;
|
|
31488
|
+
return Math.max(0.01, density);
|
|
31489
|
+
}
|
|
31490
|
+
function generateArrangement(bible, options = {}) {
|
|
31491
|
+
const opts = { ...defaultGenerateOptions, ...options };
|
|
31492
|
+
const rng = new RNG(opts.seed);
|
|
31493
|
+
const arrangement = {};
|
|
31494
|
+
const books = [];
|
|
31495
|
+
bible.testaments.forEach((t) => {
|
|
31496
|
+
t.divisions.forEach((d) => {
|
|
31497
|
+
d.books.forEach((b) => {
|
|
31498
|
+
books.push({
|
|
31499
|
+
key: b.key,
|
|
31500
|
+
name: b.name,
|
|
31501
|
+
chapters: b.chapters,
|
|
31502
|
+
division: d.name,
|
|
31503
|
+
testament: t.name
|
|
31504
|
+
});
|
|
31505
|
+
});
|
|
31506
|
+
});
|
|
31507
|
+
});
|
|
31508
|
+
const bookCount = books.length;
|
|
31509
|
+
const mwRad = THREE5__namespace.MathUtils.degToRad(opts.milkyWayAngle);
|
|
31510
|
+
const mwNormal = new THREE5__namespace.Vector3(Math.sin(mwRad), Math.cos(mwRad), 0).normalize();
|
|
31511
|
+
const anchors = [];
|
|
31512
|
+
for (let i = 0; i < bookCount; i++) {
|
|
31513
|
+
let bestP = new THREE5__namespace.Vector3();
|
|
31514
|
+
let valid = false;
|
|
31515
|
+
let attempt = 0;
|
|
31516
|
+
while (!valid && attempt < 100) {
|
|
31517
|
+
const p = rng.randomOnSphere();
|
|
31518
|
+
const d = getDensity(p, opts, mwNormal);
|
|
31519
|
+
if (rng.next() < d) {
|
|
31520
|
+
bestP = p;
|
|
31521
|
+
valid = true;
|
|
31522
|
+
}
|
|
31523
|
+
attempt++;
|
|
31524
|
+
}
|
|
31525
|
+
if (!valid) bestP = rng.randomOnSphere();
|
|
31526
|
+
anchors.push(bestP);
|
|
31527
|
+
}
|
|
31528
|
+
anchors.sort((a, b) => {
|
|
31529
|
+
const lonA = Math.atan2(a.z, a.x);
|
|
31530
|
+
const lonB = Math.atan2(b.z, b.x);
|
|
31531
|
+
return lonA - lonB;
|
|
31532
|
+
});
|
|
31533
|
+
books.forEach((book, i) => {
|
|
31534
|
+
const anchor = anchors[i];
|
|
31535
|
+
const anchorPos = anchor.clone().multiplyScalar(opts.discRadius);
|
|
31536
|
+
arrangement[`B:${book.key}`] = { position: [anchorPos.x, anchorPos.y, anchorPos.z] };
|
|
31537
|
+
for (let c = 0; c < book.chapters; c++) {
|
|
31538
|
+
const localSpread = opts.clusterSpread * (0.8 + rng.next() * 0.4);
|
|
31539
|
+
const offset = new THREE5__namespace.Vector3(
|
|
31540
|
+
(rng.next() - 0.5) * 2,
|
|
31541
|
+
(rng.next() - 0.5) * 2,
|
|
31542
|
+
(rng.next() - 0.5) * 2
|
|
31543
|
+
).normalize().multiplyScalar(rng.next() * localSpread);
|
|
31544
|
+
const starDir = anchor.clone().add(offset).normalize();
|
|
31545
|
+
if (starDir.y < 0.01) {
|
|
31546
|
+
starDir.y = 0.01;
|
|
31547
|
+
starDir.normalize();
|
|
31548
|
+
}
|
|
31549
|
+
const starPos = starDir.multiplyScalar(opts.discRadius);
|
|
31550
|
+
const chapId = `C:${book.key}:${c + 1}`;
|
|
31551
|
+
arrangement[chapId] = { position: [starPos.x, starPos.y, starPos.z] };
|
|
31552
|
+
}
|
|
31553
|
+
});
|
|
31554
|
+
const divisions = /* @__PURE__ */ new Map();
|
|
31555
|
+
books.forEach((book, i) => {
|
|
31556
|
+
const anchor = anchors[i];
|
|
31557
|
+
const anchorPos = anchor.clone().multiplyScalar(opts.discRadius);
|
|
31558
|
+
const divId = `D:${book.testament}:${book.division}`;
|
|
31559
|
+
if (!divisions.has(divId)) {
|
|
31560
|
+
divisions.set(divId, { sum: new THREE5__namespace.Vector3(), count: 0 });
|
|
31561
|
+
}
|
|
31562
|
+
const entry = divisions.get(divId);
|
|
31563
|
+
entry.sum.add(anchorPos);
|
|
31564
|
+
entry.count++;
|
|
31565
|
+
});
|
|
31566
|
+
divisions.forEach((val, key) => {
|
|
31567
|
+
if (val.count > 0) {
|
|
31568
|
+
val.sum.divideScalar(val.count);
|
|
31569
|
+
val.sum.normalize().multiplyScalar(opts.discRadius * 0.9);
|
|
31570
|
+
arrangement[key] = { position: [val.sum.x, val.sum.y, val.sum.z] };
|
|
31571
|
+
}
|
|
31572
|
+
});
|
|
31573
|
+
return arrangement;
|
|
31574
|
+
}
|
|
30493
31575
|
|
|
30494
31576
|
exports.StarMap = StarMap;
|
|
30495
31577
|
exports.bibleToSceneModel = bibleToSceneModel;
|
|
31578
|
+
exports.defaultGenerateOptions = defaultGenerateOptions;
|
|
30496
31579
|
exports.defaultStars = default_stars_default;
|
|
31580
|
+
exports.generateArrangement = generateArrangement;
|
|
30497
31581
|
//# sourceMappingURL=index.cjs.map
|
|
30498
31582
|
//# sourceMappingURL=index.cjs.map
|