@project-skymap/library 0.0.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 ADDED
@@ -0,0 +1,1003 @@
1
+ 'use strict';
2
+
3
+ var THREE4 = require('three');
4
+ var OrbitControls = require('three/examples/jsm/controls/OrbitControls');
5
+ var react = require('react');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+
8
+ function _interopNamespace(e) {
9
+ if (e && e.__esModule) return e;
10
+ var n = Object.create(null);
11
+ if (e) {
12
+ Object.keys(e).forEach(function (k) {
13
+ if (k !== 'default') {
14
+ var d = Object.getOwnPropertyDescriptor(e, k);
15
+ Object.defineProperty(n, k, d.get ? d : {
16
+ enumerable: true,
17
+ get: function () { return e[k]; }
18
+ });
19
+ }
20
+ });
21
+ }
22
+ n.default = e;
23
+ return Object.freeze(n);
24
+ }
25
+
26
+ var THREE4__namespace = /*#__PURE__*/_interopNamespace(THREE4);
27
+
28
+ var __defProp = Object.defineProperty;
29
+ var __getOwnPropNames = Object.getOwnPropertyNames;
30
+ var __esm = (fn, res) => function __init() {
31
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
32
+ };
33
+ var __export = (target, all) => {
34
+ for (var name in all)
35
+ __defProp(target, name, { get: all[name], enumerable: true });
36
+ };
37
+ function layoutSpiral(count, radius) {
38
+ const points = [];
39
+ const goldenAngle = Math.PI * (3 - Math.sqrt(5));
40
+ for (let i = 0; i < count; i++) {
41
+ const r = radius * Math.sqrt((i + 1) / count);
42
+ const theta = i * goldenAngle;
43
+ const x = r * Math.cos(theta);
44
+ const z = r * Math.sin(theta);
45
+ const y = (Math.random() - 0.5) * (radius * 0.1);
46
+ points.push({ x, y, z });
47
+ }
48
+ return points;
49
+ }
50
+ function layoutCross(count, radius) {
51
+ const points = [];
52
+ const verticalCount = Math.ceil(count * 0.7);
53
+ const horizontalCount = count - verticalCount;
54
+ const height = radius * 2.5;
55
+ const width = radius * 1.5;
56
+ for (let i = 0; i < verticalCount; i++) {
57
+ const t = i / (verticalCount - 1 || 1);
58
+ const y = height / 2 - t * height;
59
+ points.push({ x: 0, y, z: 0 });
60
+ }
61
+ const crossY = height / 2 - height * 0.3;
62
+ for (let i = 0; i < horizontalCount; i++) {
63
+ const t = i / (horizontalCount - 1 || 1);
64
+ const x = -width / 2 + t * width;
65
+ points.push({ x, y: crossY, z: 0 });
66
+ }
67
+ return points;
68
+ }
69
+ function layoutTablet(count, radius) {
70
+ const points = [];
71
+ const cols = 2;
72
+ const rows = Math.ceil(count / cols);
73
+ const w = radius * 1.5;
74
+ const h = radius * 2;
75
+ for (let i = 0; i < count; i++) {
76
+ const col = i % cols;
77
+ const row = Math.floor(i / cols);
78
+ const x = (col === 0 ? -1 : 1) * (w * 0.25);
79
+ const y = h / 2 - row / (rows - 1 || 1) * h;
80
+ points.push({ x, y, z: 0 });
81
+ }
82
+ return points;
83
+ }
84
+ function layoutCrown(count, radius) {
85
+ const points = [];
86
+ for (let i = 0; i < count; i++) {
87
+ const t = i / (count - 1 || 1);
88
+ const angle = -Math.PI * 0.8 + t * Math.PI * 1.6;
89
+ let r = radius;
90
+ if (i % 3 === 1) r *= 1.4;
91
+ const x = r * Math.sin(angle);
92
+ const y = r * Math.cos(angle) * 0.5;
93
+ points.push({ x, y, z: 0 });
94
+ }
95
+ return points;
96
+ }
97
+ function layoutHarp(count, radius) {
98
+ const points = [];
99
+ for (let i = 0; i < count; i++) {
100
+ const t = i / (count - 1 || 1);
101
+ const x = -radius + t * (radius * 2);
102
+ const y = -radius + Math.pow(t, 2) * (radius * 2);
103
+ points.push({ x, y, z: 0 });
104
+ }
105
+ return points;
106
+ }
107
+ function layoutFlame(count, radius) {
108
+ const points = [];
109
+ for (let i = 0; i < count; i++) {
110
+ const tNorm = i / count;
111
+ const r = radius * (1 - tNorm) * 0.8;
112
+ const angle = tNorm * Math.PI * 4;
113
+ const y = (tNorm - 0.5) * radius * 3;
114
+ const x = r * Math.cos(angle);
115
+ points.push({ x, y, z: 0 });
116
+ }
117
+ return points;
118
+ }
119
+ function getConstellationLayout(bookKey, chapterCount, radius) {
120
+ const generator = LAYOUT_REGISTRY[bookKey] || layoutSpiral;
121
+ return generator(chapterCount, radius);
122
+ }
123
+ var LAYOUT_REGISTRY;
124
+ var init_constellations = __esm({
125
+ "src/engine/constellations.ts"() {
126
+ LAYOUT_REGISTRY = {
127
+ // Law -> Tablet
128
+ "GEN": layoutTablet,
129
+ "EXO": layoutTablet,
130
+ "LEV": layoutTablet,
131
+ "NUM": layoutTablet,
132
+ "DEU": layoutTablet,
133
+ // Kings/History -> Crown
134
+ "1SA": layoutCrown,
135
+ "2SA": layoutCrown,
136
+ "1KI": layoutCrown,
137
+ "2KI": layoutCrown,
138
+ "1CH": layoutCrown,
139
+ "2CH": layoutCrown,
140
+ // Poetry -> Harp/Spiral
141
+ "PSA": layoutHarp,
142
+ "SNG": layoutHarp,
143
+ // Gospels -> Cross
144
+ "MAT": layoutCross,
145
+ "MRK": layoutCross,
146
+ "LUK": layoutCross,
147
+ "JHN": layoutCross,
148
+ // Acts -> Flame
149
+ "ACT": layoutFlame
150
+ };
151
+ }
152
+ });
153
+ function lookAt(point, target, up) {
154
+ const zAxis = target.clone().normalize();
155
+ const xAxis = new THREE4__namespace.Vector3().crossVectors(up, zAxis).normalize();
156
+ const yAxis = new THREE4__namespace.Vector3().crossVectors(zAxis, xAxis).normalize();
157
+ const m = new THREE4__namespace.Matrix4().makeBasis(xAxis, yAxis, zAxis);
158
+ const v = new THREE4__namespace.Vector3(point.x, point.y, point.z);
159
+ v.applyMatrix4(m);
160
+ v.add(target);
161
+ return { x: v.x, y: v.y, z: v.z };
162
+ }
163
+ function computeLayoutPositions(model, layout) {
164
+ const mode = layout?.mode ?? "spherical";
165
+ const radius = layout?.radius ?? 2e3;
166
+ const ringSpacing = layout?.chapterRingSpacing ?? 15;
167
+ const childrenMap = /* @__PURE__ */ new Map();
168
+ const roots = [];
169
+ const books = [];
170
+ for (const n of model.nodes) {
171
+ if (n.level === 2) books.push(n);
172
+ if (n.parent) {
173
+ const children = childrenMap.get(n.parent) ?? [];
174
+ children.push(n);
175
+ childrenMap.set(n.parent, children);
176
+ } else {
177
+ roots.push(n);
178
+ }
179
+ }
180
+ const updatedNodes = model.nodes.map((n) => ({ ...n, meta: { ...n.meta ?? {} } }));
181
+ const updatedNodeMap = new Map(updatedNodes.map((n) => [n.id, n]));
182
+ const leafCounts = /* @__PURE__ */ new Map();
183
+ function getLeafCount(node) {
184
+ const children = childrenMap.get(node.id) ?? [];
185
+ if (children.length === 0) {
186
+ leafCounts.set(node.id, 1);
187
+ return 1;
188
+ }
189
+ let count = 0;
190
+ for (const c of children) count += getLeafCount(c);
191
+ leafCounts.set(node.id, count);
192
+ return count;
193
+ }
194
+ roots.forEach(getLeafCount);
195
+ if (mode === "spherical") {
196
+ const numBooks = books.length;
197
+ const phi = Math.PI * (3 - Math.sqrt(5));
198
+ for (let i = 0; i < numBooks; i++) {
199
+ const book = books[i];
200
+ const uBook = updatedNodeMap.get(book.id);
201
+ const bookKey = uBook.meta.bookKey;
202
+ const y = 1 - i / (numBooks - 1) * 0.95;
203
+ const radiusAtY = Math.sqrt(1 - y * y);
204
+ const theta = phi * i;
205
+ const x = Math.cos(theta) * radiusAtY;
206
+ const z = Math.sin(theta) * radiusAtY;
207
+ const bookPos = new THREE4__namespace.Vector3(x, y, z).multiplyScalar(radius);
208
+ uBook.meta.x = bookPos.x;
209
+ uBook.meta.y = bookPos.y;
210
+ uBook.meta.z = bookPos.z;
211
+ const chapters = childrenMap.get(book.id) ?? [];
212
+ if (chapters.length > 0) {
213
+ const territoryRadius = radius * 2 / Math.sqrt(numBooks * 2) * 0.7;
214
+ const localPoints = getConstellationLayout(bookKey, chapters.length, territoryRadius);
215
+ const up = new THREE4__namespace.Vector3(0, 1, 0);
216
+ chapters.forEach((chap, idx) => {
217
+ const uChap = updatedNodeMap.get(chap.id);
218
+ const lp = localPoints[idx];
219
+ const wp = lookAt(lp, bookPos, up);
220
+ uChap.meta.x = wp.x;
221
+ uChap.meta.y = wp.y;
222
+ uChap.meta.z = wp.z;
223
+ });
224
+ }
225
+ }
226
+ const divisions = model.nodes.filter((n) => n.level === 1);
227
+ divisions.forEach((d) => {
228
+ const children = childrenMap.get(d.id) ?? [];
229
+ if (children.length === 0) return;
230
+ const centroid = new THREE4__namespace.Vector3();
231
+ children.forEach((c) => {
232
+ const u = updatedNodeMap.get(c.id);
233
+ centroid.add(new THREE4__namespace.Vector3(u.meta.x, u.meta.y, u.meta.z));
234
+ });
235
+ centroid.divideScalar(children.length);
236
+ if (centroid.length() > 0.1) {
237
+ centroid.setLength(radius * 0.85);
238
+ const uNode = updatedNodeMap.get(d.id);
239
+ uNode.meta.x = centroid.x;
240
+ uNode.meta.y = centroid.y;
241
+ uNode.meta.z = centroid.z;
242
+ }
243
+ });
244
+ const testaments = model.nodes.filter((n) => n.level === 0);
245
+ testaments.forEach((t) => {
246
+ const children = childrenMap.get(t.id) ?? [];
247
+ if (children.length === 0) return;
248
+ const centroid = new THREE4__namespace.Vector3();
249
+ children.forEach((c) => {
250
+ const u = updatedNodeMap.get(c.id);
251
+ centroid.add(new THREE4__namespace.Vector3(u.meta.x, u.meta.y, u.meta.z));
252
+ });
253
+ centroid.divideScalar(children.length);
254
+ if (centroid.length() > 0.1) {
255
+ centroid.setLength(radius * 0.75);
256
+ const uNode = updatedNodeMap.get(t.id);
257
+ uNode.meta.x = centroid.x;
258
+ uNode.meta.y = centroid.y;
259
+ uNode.meta.z = centroid.z;
260
+ }
261
+ });
262
+ return { ...model, nodes: updatedNodes };
263
+ }
264
+ function layoutRadial(nodes, startAngle, totalAngle, level) {
265
+ if (nodes.length === 0) return;
266
+ nodes.sort((a, b) => a.id.localeCompare(b.id));
267
+ const totalLeaves = nodes.reduce((sum, n) => sum + (leafCounts.get(n.id) ?? 1), 0);
268
+ let currentAngle = startAngle;
269
+ const currentRadius = level === 0 ? 0 : radius + (level - 1) * ringSpacing;
270
+ for (const node of nodes) {
271
+ const weight = leafCounts.get(node.id) ?? 1;
272
+ const nodeAngleSpan = weight / totalLeaves * totalAngle;
273
+ const midAngle = currentAngle + nodeAngleSpan / 2;
274
+ const updatedNode = updatedNodeMap.get(node.id);
275
+ const r = level === 0 ? 0 : currentRadius;
276
+ updatedNode.meta.x = Math.cos(midAngle) * r;
277
+ updatedNode.meta.y = Math.sin(midAngle) * r;
278
+ updatedNode.meta.z = 0;
279
+ updatedNode.meta.angle = midAngle;
280
+ const children = childrenMap.get(node.id) ?? [];
281
+ if (children.length > 0) {
282
+ layoutRadial(children, currentAngle, nodeAngleSpan, level + 1);
283
+ }
284
+ currentAngle += nodeAngleSpan;
285
+ }
286
+ }
287
+ layoutRadial(roots, 0, Math.PI * 2, 0);
288
+ return { ...model, nodes: updatedNodes };
289
+ }
290
+ var init_layout = __esm({
291
+ "src/engine/layout.ts"() {
292
+ init_constellations();
293
+ }
294
+ });
295
+ function matches(node, when) {
296
+ for (const [k, v] of Object.entries(when)) {
297
+ const val = node[k] !== void 0 ? node[k] : node.meta?.[k];
298
+ if (val !== v) return false;
299
+ }
300
+ return true;
301
+ }
302
+ function applyVisuals({
303
+ model,
304
+ cfg,
305
+ meshById
306
+ }) {
307
+ const colorRules = cfg.visuals?.colorBy ?? [];
308
+ const sizeRules = cfg.visuals?.sizeBy ?? [];
309
+ const weights = model.nodes.map((n) => n.weight).filter((x) => typeof x === "number");
310
+ const minW = weights.length ? Math.min(...weights) : 0;
311
+ const maxW = weights.length ? Math.max(...weights) : 1;
312
+ const range = Math.max(0, maxW - minW);
313
+ for (const node of model.nodes) {
314
+ const mesh = meshById.get(node.id);
315
+ if (!mesh) continue;
316
+ let color;
317
+ for (const rule of colorRules) {
318
+ if (matches(node, rule.when)) {
319
+ color = rule.value;
320
+ break;
321
+ }
322
+ }
323
+ if (color && mesh.material?.color) {
324
+ mesh.material.color = new THREE4__namespace.Color(color);
325
+ }
326
+ for (const rule of sizeRules) {
327
+ if (!matches(node, rule.when)) continue;
328
+ const w = node[rule.field];
329
+ if (typeof w !== "number") continue;
330
+ const t = range === 0 ? 0.5 : (w - minW) / range;
331
+ const clamped = Math.min(1, Math.max(0, t));
332
+ const s = rule.scale[0] + t * (rule.scale[1] - rule.scale[0]);
333
+ mesh.scale.setScalar(clamped === t ? s : rule.scale[0] + clamped * (rule.scale[1] - rule.scale[0]));
334
+ break;
335
+ }
336
+ }
337
+ }
338
+ var init_materials = __esm({
339
+ "src/engine/materials.ts"() {
340
+ }
341
+ });
342
+
343
+ // src/engine/createEngine.ts
344
+ var createEngine_exports = {};
345
+ __export(createEngine_exports, {
346
+ createEngine: () => createEngine
347
+ });
348
+ function createEngine({
349
+ container,
350
+ onSelect,
351
+ onHover
352
+ }) {
353
+ const renderer = new THREE4__namespace.WebGLRenderer({ antialias: true });
354
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
355
+ renderer.setClearColor(0, 1);
356
+ container.appendChild(renderer.domElement);
357
+ renderer.domElement.style.touchAction = "none";
358
+ const scene = new THREE4__namespace.Scene();
359
+ const camera = new THREE4__namespace.PerspectiveCamera(90, 1, 0.1, 5e3);
360
+ camera.position.set(0, 0, 0.01);
361
+ camera.up.set(0, 1, 0);
362
+ const controls = new OrbitControls.OrbitControls(camera, renderer.domElement);
363
+ controls.target.set(0, 0, 0);
364
+ controls.enableRotate = true;
365
+ controls.enablePan = false;
366
+ controls.enableZoom = false;
367
+ controls.enableDamping = true;
368
+ controls.dampingFactor = 0.07;
369
+ controls.screenSpacePanning = false;
370
+ const EPS = THREE4__namespace.MathUtils.degToRad(0.05);
371
+ controls.minAzimuthAngle = -Infinity;
372
+ controls.maxAzimuthAngle = Infinity;
373
+ controls.minPolarAngle = Math.PI / 2 + EPS;
374
+ controls.maxPolarAngle = Math.PI - EPS;
375
+ controls.update();
376
+ const env = {
377
+ skyRadius: 2800,
378
+ groundRadius: 995,
379
+ defaultFov: 90,
380
+ minFov: 1,
381
+ maxFov: 110,
382
+ fovWheelSensitivity: 0.04,
383
+ focusZoomFov: 18,
384
+ focusDurationMs: 650
385
+ };
386
+ const groundGroup = new THREE4__namespace.Group();
387
+ scene.add(groundGroup);
388
+ function buildGroundHemisphere(radius) {
389
+ groundGroup.clear();
390
+ const hemi = new THREE4__namespace.SphereGeometry(radius, 64, 32, 0, Math.PI * 2, Math.PI / 2, Math.PI);
391
+ hemi.scale(-1, 1, 1);
392
+ const count = hemi.attributes.position.count;
393
+ const colors = new Float32Array(count * 3);
394
+ const pos = hemi.attributes.position;
395
+ const c = new THREE4__namespace.Color();
396
+ for (let i = 0; i < count; i++) {
397
+ const y = pos.getY(i);
398
+ const t = THREE4__namespace.MathUtils.clamp(1 - Math.abs(y) / radius, 0, 1);
399
+ c.setRGB(
400
+ THREE4__namespace.MathUtils.lerp(6 / 255, 21 / 255, t),
401
+ THREE4__namespace.MathUtils.lerp(16 / 255, 35 / 255, t),
402
+ THREE4__namespace.MathUtils.lerp(23 / 255, 53 / 255, t)
403
+ );
404
+ colors.set([c.r, c.g, c.b], i * 3);
405
+ }
406
+ hemi.setAttribute("color", new THREE4__namespace.BufferAttribute(colors, 3));
407
+ const groundMat = new THREE4__namespace.MeshBasicMaterial({
408
+ vertexColors: true,
409
+ side: THREE4__namespace.FrontSide,
410
+ depthWrite: true,
411
+ // important: occlude stars under horizon
412
+ depthTest: true
413
+ });
414
+ const hemiMesh = new THREE4__namespace.Mesh(hemi, groundMat);
415
+ groundGroup.add(hemiMesh);
416
+ {
417
+ const inner = radius * 0.985;
418
+ const outer = radius * 1.005;
419
+ const ringGeo = new THREE4__namespace.RingGeometry(inner, outer, 128);
420
+ ringGeo.rotateX(-Math.PI / 2);
421
+ const ringMat = new THREE4__namespace.ShaderMaterial({
422
+ transparent: true,
423
+ depthWrite: false,
424
+ uniforms: {
425
+ uInner: { value: inner },
426
+ uOuter: { value: outer },
427
+ uColor: { value: new THREE4__namespace.Color(8037119) }
428
+ },
429
+ vertexShader: `
430
+ varying vec2 vXY;
431
+ void main() {
432
+ vXY = position.xz;
433
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
434
+ }
435
+ `,
436
+ fragmentShader: `
437
+ uniform float uInner;
438
+ uniform float uOuter;
439
+ uniform vec3 uColor;
440
+ varying vec2 vXY;
441
+
442
+ void main() {
443
+ float r = length(vXY);
444
+ float t = clamp((r - uInner) / (uOuter - uInner), 0.0, 1.0);
445
+ // peak glow near inner edge, fade outwards
446
+ float a = pow(1.0 - t, 2.2) * 0.35;
447
+ gl_FragColor = vec4(uColor, a);
448
+ }
449
+ `
450
+ });
451
+ const ring = new THREE4__namespace.Mesh(ringGeo, ringMat);
452
+ ring.position.y = 0;
453
+ groundGroup.add(ring);
454
+ }
455
+ }
456
+ buildGroundHemisphere(env.groundRadius);
457
+ const backdropGroup = new THREE4__namespace.Group();
458
+ scene.add(backdropGroup);
459
+ function buildBackdropStars(radius, count) {
460
+ backdropGroup.clear();
461
+ const starGeo = new THREE4__namespace.BufferGeometry();
462
+ const starPos = new Float32Array(count * 3);
463
+ for (let i = 0; i < count; i++) {
464
+ const r = radius + Math.random() * (radius * 0.5);
465
+ const theta = Math.random() * Math.PI * 2;
466
+ const phi = Math.acos(2 * Math.random() - 1);
467
+ starPos[i * 3] = r * Math.sin(phi) * Math.cos(theta);
468
+ starPos[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
469
+ starPos[i * 3 + 2] = r * Math.cos(phi);
470
+ }
471
+ starGeo.setAttribute("position", new THREE4__namespace.BufferAttribute(starPos, 3));
472
+ const starMat = new THREE4__namespace.PointsMaterial({
473
+ color: 10135218,
474
+ size: 0.5,
475
+ transparent: true,
476
+ opacity: 0.55,
477
+ depthWrite: false
478
+ });
479
+ const stars = new THREE4__namespace.Points(starGeo, starMat);
480
+ backdropGroup.add(stars);
481
+ }
482
+ buildBackdropStars(env.skyRadius, 6500);
483
+ const raycaster = new THREE4__namespace.Raycaster();
484
+ const pointer = new THREE4__namespace.Vector2();
485
+ const root = new THREE4__namespace.Group();
486
+ scene.add(root);
487
+ let raf = 0;
488
+ let running = false;
489
+ let handlers = { onSelect, onHover };
490
+ let hoveredId = null;
491
+ let isDragging = false;
492
+ const nodeById = /* @__PURE__ */ new Map();
493
+ const meshById = /* @__PURE__ */ new Map();
494
+ const lineByBookId = /* @__PURE__ */ new Map();
495
+ const dynamicObjects = [];
496
+ function resize() {
497
+ const w = container.clientWidth || 1;
498
+ const h = container.clientHeight || 1;
499
+ renderer.setSize(w, h, false);
500
+ camera.aspect = w / h;
501
+ camera.updateProjectionMatrix();
502
+ }
503
+ function disposeObject(obj) {
504
+ obj.traverse((o) => {
505
+ if (o.geometry) o.geometry.dispose?.();
506
+ if (o.material) {
507
+ const mats = Array.isArray(o.material) ? o.material : [o.material];
508
+ mats.forEach((m) => {
509
+ if (m.map) m.map.dispose?.();
510
+ m.dispose?.();
511
+ });
512
+ }
513
+ });
514
+ }
515
+ function createTextSprite(text, color = "#ffffff") {
516
+ const canvas = document.createElement("canvas");
517
+ const ctx = canvas.getContext("2d");
518
+ if (!ctx) return null;
519
+ const fontSize = 48;
520
+ const font = `bold ${fontSize}px sans-serif`;
521
+ ctx.font = font;
522
+ const metrics = ctx.measureText(text);
523
+ const w = Math.ceil(metrics.width);
524
+ const h = Math.ceil(fontSize * 1.2);
525
+ canvas.width = w;
526
+ canvas.height = h;
527
+ ctx.font = font;
528
+ ctx.fillStyle = color;
529
+ ctx.textAlign = "center";
530
+ ctx.textBaseline = "middle";
531
+ ctx.fillText(text, w / 2, h / 2);
532
+ const tex = new THREE4__namespace.CanvasTexture(canvas);
533
+ tex.minFilter = THREE4__namespace.LinearFilter;
534
+ const mat = new THREE4__namespace.SpriteMaterial({
535
+ map: tex,
536
+ transparent: true,
537
+ depthWrite: false,
538
+ depthTest: true
539
+ });
540
+ const sprite = new THREE4__namespace.Sprite(mat);
541
+ const targetHeight = 2;
542
+ const aspect = w / h;
543
+ sprite.scale.set(targetHeight * aspect, targetHeight, 1);
544
+ return sprite;
545
+ }
546
+ function createStarTexture() {
547
+ const canvas = document.createElement("canvas");
548
+ canvas.width = 64;
549
+ canvas.height = 64;
550
+ const ctx = canvas.getContext("2d");
551
+ const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
552
+ gradient.addColorStop(0, "rgba(255, 255, 255, 1)");
553
+ gradient.addColorStop(0.2, "rgba(255, 255, 255, 0.8)");
554
+ gradient.addColorStop(0.5, "rgba(255, 255, 255, 0.2)");
555
+ gradient.addColorStop(1, "rgba(255, 255, 255, 0)");
556
+ ctx.fillStyle = gradient;
557
+ ctx.fillRect(0, 0, 64, 64);
558
+ const tex = new THREE4__namespace.CanvasTexture(canvas);
559
+ return tex;
560
+ }
561
+ const starTexture = createStarTexture();
562
+ function clearRoot() {
563
+ for (const child of [...root.children]) {
564
+ root.remove(child);
565
+ disposeObject(child);
566
+ }
567
+ nodeById.clear();
568
+ meshById.clear();
569
+ lineByBookId.clear();
570
+ dynamicObjects.length = 0;
571
+ }
572
+ function buildFromModel(model, cfg) {
573
+ clearRoot();
574
+ if (cfg.background) scene.background = new THREE4__namespace.Color(cfg.background);
575
+ camera.fov = cfg.camera?.fov ?? env.defaultFov;
576
+ camera.updateProjectionMatrix();
577
+ const layoutCfg = {
578
+ ...cfg.layout,
579
+ radius: cfg.layout?.radius ?? 2e3
580
+ };
581
+ const laidOut = computeLayoutPositions(model, layoutCfg);
582
+ for (const n of laidOut.nodes) {
583
+ nodeById.set(n.id, n);
584
+ const x = n.meta?.x ?? 0;
585
+ const y = n.meta?.y ?? 0;
586
+ const z = n.meta?.z ?? 0;
587
+ if (n.level === 3) {
588
+ const mat = new THREE4__namespace.SpriteMaterial({
589
+ map: starTexture,
590
+ color: 16777215,
591
+ transparent: true,
592
+ blending: THREE4__namespace.AdditiveBlending,
593
+ depthWrite: false
594
+ });
595
+ const sprite = new THREE4__namespace.Sprite(mat);
596
+ sprite.position.set(x, y, z);
597
+ sprite.userData = { id: n.id, level: n.level };
598
+ const baseScale = 2;
599
+ sprite.scale.setScalar(baseScale);
600
+ dynamicObjects.push({ obj: sprite, initialScale: sprite.scale.clone(), type: "star" });
601
+ if (n.label) {
602
+ const labelSprite = createTextSprite(n.label);
603
+ if (labelSprite) {
604
+ labelSprite.position.set(0, 1.2, 0);
605
+ labelSprite.visible = false;
606
+ sprite.add(labelSprite);
607
+ }
608
+ }
609
+ root.add(sprite);
610
+ meshById.set(n.id, sprite);
611
+ } else if (n.level === 1 || n.level === 2) {
612
+ if (n.label) {
613
+ const isBook = n.level === 2;
614
+ const color = isBook ? "#ffffff" : "#38bdf8";
615
+ const baseScale = isBook ? 3 : 7;
616
+ const labelSprite = createTextSprite(n.label, color);
617
+ if (labelSprite) {
618
+ labelSprite.position.set(x, y, z);
619
+ labelSprite.scale.multiplyScalar(baseScale);
620
+ root.add(labelSprite);
621
+ if (isBook) {
622
+ dynamicObjects.push({ obj: labelSprite, initialScale: labelSprite.scale.clone(), type: "label" });
623
+ }
624
+ }
625
+ }
626
+ }
627
+ }
628
+ applyVisuals({ model: laidOut, cfg, meshById });
629
+ const lineMat = new THREE4__namespace.LineBasicMaterial({
630
+ color: 4478310,
631
+ transparent: true,
632
+ opacity: 0.3,
633
+ depthWrite: false,
634
+ blending: THREE4__namespace.AdditiveBlending
635
+ });
636
+ const bookMap = /* @__PURE__ */ new Map();
637
+ for (const n of laidOut.nodes) {
638
+ if (n.level === 3 && n.parent) {
639
+ const list = bookMap.get(n.parent) ?? [];
640
+ list.push(n);
641
+ bookMap.set(n.parent, list);
642
+ }
643
+ }
644
+ for (const [bookId, chapters] of bookMap.entries()) {
645
+ chapters.sort((a, b) => {
646
+ const cA = a.meta?.chapter || 0;
647
+ const cB = b.meta?.chapter || 0;
648
+ return cA - cB;
649
+ });
650
+ if (chapters.length < 2) continue;
651
+ const points = [];
652
+ for (const c of chapters) {
653
+ const x = c.meta?.x ?? 0;
654
+ const y = c.meta?.y ?? 0;
655
+ const z = c.meta?.z ?? 0;
656
+ points.push(new THREE4__namespace.Vector3(x, y, z));
657
+ }
658
+ const geo = new THREE4__namespace.BufferGeometry().setFromPoints(points);
659
+ const line = new THREE4__namespace.Line(geo, lineMat);
660
+ root.add(line);
661
+ lineByBookId.set(bookId, line);
662
+ }
663
+ resize();
664
+ }
665
+ function setConfig(cfg) {
666
+ let model = cfg.model;
667
+ if (!model && cfg.data && cfg.adapter) model = cfg.adapter(cfg.data);
668
+ if (!model) {
669
+ clearRoot();
670
+ return;
671
+ }
672
+ buildFromModel(model, cfg);
673
+ }
674
+ function setHandlers(next) {
675
+ handlers = next;
676
+ }
677
+ function pick(ev) {
678
+ const rect = renderer.domElement.getBoundingClientRect();
679
+ pointer.x = (ev.clientX - rect.left) / rect.width * 2 - 1;
680
+ pointer.y = -((ev.clientY - rect.top) / rect.height * 2 - 1);
681
+ raycaster.setFromCamera(pointer, camera);
682
+ const hits = raycaster.intersectObjects(root.children, true);
683
+ const hit = hits.find((h) => h.object.type === "Mesh" || h.object.type === "Sprite");
684
+ const id = hit?.object?.userData?.id;
685
+ return id ? nodeById.get(id) : void 0;
686
+ }
687
+ function onPointerMove(ev) {
688
+ const node = pick(ev);
689
+ const nextId = node?.id ?? null;
690
+ if (nextId !== hoveredId) {
691
+ if (hoveredId) {
692
+ const prevMesh = meshById.get(hoveredId);
693
+ const prevNode = nodeById.get(hoveredId);
694
+ if (prevMesh && prevNode && prevNode.level === 3) {
695
+ const label = prevMesh.children.find((c) => c instanceof THREE4__namespace.Sprite);
696
+ if (label) label.visible = false;
697
+ }
698
+ }
699
+ hoveredId = nextId;
700
+ if (nextId) {
701
+ const mesh = meshById.get(nextId);
702
+ const n = nodeById.get(nextId);
703
+ if (mesh && n && n.level === 3) {
704
+ const label = mesh.children.find((c) => c instanceof THREE4__namespace.Sprite);
705
+ if (label) {
706
+ label.visible = true;
707
+ label.position.set(0, 0.8, 0);
708
+ }
709
+ if (n.parent) {
710
+ const line = lineByBookId.get(n.parent);
711
+ if (line) line.material.opacity = 0.8;
712
+ }
713
+ }
714
+ }
715
+ handlers.onHover?.(node);
716
+ }
717
+ }
718
+ function onPointerDown() {
719
+ isDragging = false;
720
+ }
721
+ function onChange() {
722
+ isDragging = true;
723
+ }
724
+ const onWheelFov = (ev) => {
725
+ ev.preventDefault();
726
+ const speed = (ev.ctrlKey ? 0.15 : 1) * env.fovWheelSensitivity;
727
+ const next = THREE4__namespace.MathUtils.clamp(
728
+ camera.fov + ev.deltaY * speed,
729
+ env.minFov,
730
+ env.maxFov
731
+ );
732
+ if (next !== camera.fov) {
733
+ camera.fov = next;
734
+ camera.updateProjectionMatrix();
735
+ }
736
+ };
737
+ const onDblClick = (ev) => {
738
+ const node = pick(ev);
739
+ if (node) {
740
+ const mesh = meshById.get(node.id);
741
+ if (mesh) {
742
+ animateFocusTo(mesh);
743
+ return;
744
+ }
745
+ }
746
+ camera.fov = env.defaultFov;
747
+ camera.updateProjectionMatrix();
748
+ };
749
+ let focusAnimRaf = 0;
750
+ function cancelFocusAnim() {
751
+ if (focusAnimRaf) cancelAnimationFrame(focusAnimRaf);
752
+ focusAnimRaf = 0;
753
+ }
754
+ function getControlsAnglesSafe() {
755
+ const getAz = controls.getAzimuthalAngle?.bind(controls);
756
+ const getPol = controls.getPolarAngle?.bind(controls);
757
+ return {
758
+ azimuth: typeof getAz === "function" ? getAz() : 0,
759
+ polar: typeof getPol === "function" ? getPol() : Math.PI / 4
760
+ };
761
+ }
762
+ function setControlsAnglesSafe(azimuth, polar) {
763
+ const setAz = controls.setAzimuthalAngle?.bind(controls);
764
+ const setPol = controls.setPolarAngle?.bind(controls);
765
+ if (typeof setAz === "function" && typeof setPol === "function") {
766
+ setAz(azimuth);
767
+ setPol(polar);
768
+ controls.update();
769
+ return;
770
+ }
771
+ const dir = new THREE4__namespace.Vector3();
772
+ dir.setFromSphericalCoords(1, polar, azimuth);
773
+ const lookAt2 = dir.clone().multiplyScalar(10);
774
+ camera.lookAt(lookAt2);
775
+ }
776
+ function aimAtWorldPoint(worldPoint) {
777
+ const dir = worldPoint.clone().normalize().negate();
778
+ const spherical = new THREE4__namespace.Spherical().setFromVector3(dir);
779
+ let targetPolar = spherical.phi;
780
+ let targetAz = spherical.theta;
781
+ targetPolar = THREE4__namespace.MathUtils.clamp(targetPolar, controls.minPolarAngle, controls.maxPolarAngle);
782
+ return { targetAz, targetPolar };
783
+ }
784
+ function easeInOutCubic(t) {
785
+ return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
786
+ }
787
+ function animateFocusTo(mesh) {
788
+ cancelFocusAnim();
789
+ const { azimuth: startAz, polar: startPolar } = getControlsAnglesSafe();
790
+ const startFov = camera.fov;
791
+ const { targetAz, targetPolar } = aimAtWorldPoint(mesh.getWorldPosition(new THREE4__namespace.Vector3()));
792
+ const endFov = THREE4__namespace.MathUtils.clamp(env.focusZoomFov, env.minFov, env.maxFov);
793
+ const start2 = performance.now();
794
+ const dur = Math.max(120, env.focusDurationMs);
795
+ const tick = (now) => {
796
+ const t = THREE4__namespace.MathUtils.clamp((now - start2) / dur, 0, 1);
797
+ const k = easeInOutCubic(t);
798
+ let dAz = targetAz - startAz;
799
+ dAz = (dAz + Math.PI) % (Math.PI * 2) - Math.PI;
800
+ const curAz = startAz + dAz * k;
801
+ const curPolar = THREE4__namespace.MathUtils.lerp(startPolar, targetPolar, k);
802
+ setControlsAnglesSafe(curAz, curPolar);
803
+ camera.fov = THREE4__namespace.MathUtils.lerp(startFov, endFov, k);
804
+ camera.updateProjectionMatrix();
805
+ if (t < 1) {
806
+ focusAnimRaf = requestAnimationFrame(tick);
807
+ } else {
808
+ focusAnimRaf = 0;
809
+ }
810
+ };
811
+ focusAnimRaf = requestAnimationFrame(tick);
812
+ }
813
+ function onPointerUp(ev) {
814
+ if (isDragging) return;
815
+ const node = pick(ev);
816
+ if (!node) return;
817
+ handlers.onSelect?.(node);
818
+ }
819
+ function start() {
820
+ if (running) return;
821
+ running = true;
822
+ resize();
823
+ window.addEventListener("resize", resize);
824
+ renderer.domElement.addEventListener("pointermove", onPointerMove);
825
+ renderer.domElement.addEventListener("pointerdown", onPointerDown);
826
+ renderer.domElement.addEventListener("pointerup", onPointerUp);
827
+ renderer.domElement.addEventListener("wheel", onWheelFov, { passive: false });
828
+ renderer.domElement.addEventListener("dblclick", onDblClick);
829
+ controls.addEventListener("change", onChange);
830
+ const tick = () => {
831
+ raf = requestAnimationFrame(tick);
832
+ controls.rotateSpeed = camera.fov / (env.defaultFov);
833
+ const fov = camera.fov;
834
+ const minZoomFov = 15;
835
+ const scaleFactor = Math.max(1, 1 + (fov - minZoomFov) * 0.05);
836
+ const cameraDir = new THREE4__namespace.Vector3();
837
+ camera.getWorldDirection(cameraDir);
838
+ const objPos = new THREE4__namespace.Vector3();
839
+ const objDir = new THREE4__namespace.Vector3();
840
+ for (let i = 0; i < dynamicObjects.length; i++) {
841
+ const item = dynamicObjects[i];
842
+ item.obj.scale.copy(item.initialScale).multiplyScalar(scaleFactor);
843
+ if (item.type === "label") {
844
+ const sprite = item.obj;
845
+ sprite.getWorldPosition(objPos);
846
+ objDir.subVectors(objPos, camera.position).normalize();
847
+ const dot = cameraDir.dot(objDir);
848
+ const fullVisibleDot = 0.96;
849
+ const invisibleDot = 0.88;
850
+ let opacity = 0;
851
+ if (dot >= fullVisibleDot) {
852
+ opacity = 1;
853
+ } else if (dot > invisibleDot) {
854
+ opacity = (dot - invisibleDot) / (fullVisibleDot - invisibleDot);
855
+ }
856
+ sprite.material.opacity = opacity;
857
+ sprite.visible = opacity > 0.01;
858
+ const bookId = nodeById.get(item.obj.userData.id)?.id;
859
+ if (bookId) {
860
+ const line = lineByBookId.get(bookId);
861
+ if (line) {
862
+ line.material.opacity = 0.05 + opacity * 0.45;
863
+ }
864
+ }
865
+ }
866
+ }
867
+ controls.update();
868
+ renderer.render(scene, camera);
869
+ };
870
+ tick();
871
+ }
872
+ function stop() {
873
+ running = false;
874
+ cancelAnimationFrame(raf);
875
+ cancelFocusAnim();
876
+ window.removeEventListener("resize", resize);
877
+ renderer.domElement.removeEventListener("pointermove", onPointerMove);
878
+ renderer.domElement.removeEventListener("pointerdown", onPointerDown);
879
+ renderer.domElement.removeEventListener("pointerup", onPointerUp);
880
+ renderer.domElement.removeEventListener("wheel", onWheelFov);
881
+ renderer.domElement.removeEventListener("dblclick", onDblClick);
882
+ controls.removeEventListener("change", onChange);
883
+ }
884
+ function dispose() {
885
+ stop();
886
+ clearRoot();
887
+ for (const child of [...groundGroup.children]) {
888
+ groundGroup.remove(child);
889
+ disposeObject(child);
890
+ }
891
+ for (const child of [...backdropGroup.children]) {
892
+ backdropGroup.remove(child);
893
+ disposeObject(child);
894
+ }
895
+ controls.dispose();
896
+ renderer.dispose();
897
+ renderer.domElement.remove();
898
+ }
899
+ return { setConfig, start, stop, dispose, setHandlers };
900
+ }
901
+ var init_createEngine = __esm({
902
+ "src/engine/createEngine.ts"() {
903
+ init_layout();
904
+ init_materials();
905
+ }
906
+ });
907
+ function StarMap({ config, className, onSelect, onHover }) {
908
+ const containerRef = react.useRef(null);
909
+ const engineRef = react.useRef(null);
910
+ react.useEffect(() => {
911
+ let disposed = false;
912
+ async function init() {
913
+ if (!containerRef.current) return;
914
+ const { createEngine: createEngine2 } = await Promise.resolve().then(() => (init_createEngine(), createEngine_exports));
915
+ if (disposed) return;
916
+ engineRef.current = createEngine2({
917
+ container: containerRef.current,
918
+ onSelect,
919
+ onHover
920
+ });
921
+ engineRef.current.setConfig(config);
922
+ engineRef.current.start();
923
+ }
924
+ init();
925
+ return () => {
926
+ disposed = true;
927
+ engineRef.current?.dispose?.();
928
+ engineRef.current = null;
929
+ };
930
+ }, []);
931
+ react.useEffect(() => {
932
+ engineRef.current?.setConfig?.(config);
933
+ }, [config]);
934
+ react.useEffect(() => {
935
+ engineRef.current?.setHandlers?.({ onSelect, onHover });
936
+ }, [onSelect, onHover]);
937
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className, style: { width: "100%", height: "100%" } });
938
+ }
939
+
940
+ // src/adapters/bible.ts
941
+ function bibleToSceneModel(data) {
942
+ const nodes = [];
943
+ const links = [];
944
+ const id = {
945
+ testament: (t) => `T:${t}`,
946
+ division: (t, d) => `D:${t}:${d}`,
947
+ book: (key) => `B:${key}`,
948
+ chapter: (key, ch) => `C:${key}:${ch}`
949
+ };
950
+ for (const t of data.testaments) {
951
+ const tid = id.testament(t.name);
952
+ nodes.push({ id: tid, label: t.name, level: 0, meta: { testament: t.name } });
953
+ for (const d of t.divisions) {
954
+ const did = id.division(t.name, d.name);
955
+ nodes.push({
956
+ id: did,
957
+ label: d.name,
958
+ level: 1,
959
+ parent: tid,
960
+ meta: { testament: t.name, division: d.name }
961
+ });
962
+ links.push({ source: did, target: tid });
963
+ for (const b of d.books) {
964
+ const bid = id.book(b.key);
965
+ nodes.push({
966
+ id: bid,
967
+ label: b.name,
968
+ level: 2,
969
+ parent: did,
970
+ meta: { testament: t.name, division: d.name, bookKey: b.key, book: b.name }
971
+ });
972
+ links.push({ source: bid, target: did });
973
+ const verses = b.verses ?? [];
974
+ for (let i = 0; i < verses.length; i++) {
975
+ const chapterNum = i + 1;
976
+ const cid = id.chapter(b.key, chapterNum);
977
+ nodes.push({
978
+ id: cid,
979
+ label: `${b.name} ${chapterNum}`,
980
+ level: 3,
981
+ parent: bid,
982
+ weight: verses[i],
983
+ icon: b.icons?.[i] ?? b.icons?.[0],
984
+ meta: {
985
+ testament: t.name,
986
+ division: d.name,
987
+ bookKey: b.key,
988
+ book: b.name,
989
+ chapter: chapterNum
990
+ }
991
+ });
992
+ links.push({ source: cid, target: bid });
993
+ }
994
+ }
995
+ }
996
+ }
997
+ return { nodes, links };
998
+ }
999
+
1000
+ exports.StarMap = StarMap;
1001
+ exports.bibleToSceneModel = bibleToSceneModel;
1002
+ //# sourceMappingURL=index.cjs.map
1003
+ //# sourceMappingURL=index.cjs.map