worldorbit 3.0.6 → 3.1.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.
Files changed (53) hide show
  1. package/README.md +2 -2
  2. package/dist/browser/core/dist/atlas-edit.js +5 -1
  3. package/dist/browser/core/dist/atlas-validate.js +1 -1
  4. package/dist/browser/core/dist/draft-parse.js +14 -9
  5. package/dist/browser/core/dist/draft.d.ts +1 -0
  6. package/dist/browser/core/dist/draft.js +4 -2
  7. package/dist/browser/core/dist/format.js +5 -3
  8. package/dist/browser/core/dist/load.js +3 -2
  9. package/dist/browser/core/dist/normalize.js +36 -0
  10. package/dist/browser/core/dist/parse.js +54 -0
  11. package/dist/browser/core/dist/scene.js +1 -0
  12. package/dist/browser/core/dist/types.d.ts +21 -1
  13. package/dist/browser/viewer/dist/runtime-3d.js +396 -117
  14. package/dist/browser/viewer/dist/theme.js +27 -0
  15. package/dist/browser/viewer/dist/types.d.ts +17 -0
  16. package/dist/browser/viewer/dist/viewer.js +51 -0
  17. package/dist/unpkg/core/dist/atlas-edit.js +5 -1
  18. package/dist/unpkg/core/dist/atlas-validate.js +1 -1
  19. package/dist/unpkg/core/dist/draft-parse.js +14 -9
  20. package/dist/unpkg/core/dist/draft.d.ts +1 -0
  21. package/dist/unpkg/core/dist/draft.js +4 -2
  22. package/dist/unpkg/core/dist/format.js +5 -3
  23. package/dist/unpkg/core/dist/load.js +3 -2
  24. package/dist/unpkg/core/dist/normalize.js +36 -0
  25. package/dist/unpkg/core/dist/parse.js +54 -0
  26. package/dist/unpkg/core/dist/scene.js +1 -0
  27. package/dist/unpkg/core/dist/types.d.ts +21 -1
  28. package/dist/unpkg/viewer/dist/runtime-3d.js +396 -117
  29. package/dist/unpkg/viewer/dist/theme.js +27 -0
  30. package/dist/unpkg/viewer/dist/types.d.ts +17 -0
  31. package/dist/unpkg/viewer/dist/viewer.js +51 -0
  32. package/dist/unpkg/worldorbit-core.min.js +9 -9
  33. package/dist/unpkg/worldorbit-editor.min.js +360 -356
  34. package/dist/unpkg/worldorbit-markdown.min.js +20 -20
  35. package/dist/unpkg/worldorbit-viewer.min.js +210 -206
  36. package/dist/unpkg/worldorbit.js +557 -120
  37. package/dist/unpkg/worldorbit.min.js +216 -212
  38. package/package.json +1 -1
  39. package/packages/core/dist/atlas-edit.js +5 -1
  40. package/packages/core/dist/atlas-validate.js +1 -1
  41. package/packages/core/dist/draft-parse.js +14 -9
  42. package/packages/core/dist/draft.d.ts +1 -0
  43. package/packages/core/dist/draft.js +4 -2
  44. package/packages/core/dist/format.js +5 -3
  45. package/packages/core/dist/load.js +3 -2
  46. package/packages/core/dist/normalize.js +36 -0
  47. package/packages/core/dist/parse.js +54 -0
  48. package/packages/core/dist/scene.js +1 -0
  49. package/packages/core/dist/types.d.ts +21 -1
  50. package/packages/viewer/dist/runtime-3d.js +396 -117
  51. package/packages/viewer/dist/theme.js +27 -0
  52. package/packages/viewer/dist/types.d.ts +17 -0
  53. package/packages/viewer/dist/viewer.js +51 -0
@@ -18,10 +18,12 @@ export function createViewer3DRuntime(container) {
18
18
  let currentVisibleObjectIds = new Set();
19
19
  let currentSelectedObjectId = null;
20
20
  let currentHoveredObjectId = null;
21
- let currentTimeSeconds = 0;
22
21
  let currentPositions = new Map();
23
22
  let pendingUpdate = null;
24
23
  let destroyed = false;
24
+ let smoothedCameraPosition = null;
25
+ let smoothedCameraTarget = null;
26
+ let currentEnvironmentKey = "";
25
27
  const objectVisuals = new Map();
26
28
  const orbitVisuals = new Map();
27
29
  const raycastTargets = [];
@@ -31,7 +33,7 @@ export function createViewer3DRuntime(container) {
31
33
  return;
32
34
  }
33
35
  const scene3d = new THREE.Scene();
34
- const camera = new THREE.PerspectiveCamera(52, 1, 0.1, 20_000);
36
+ const camera = new THREE.PerspectiveCamera(46, 1, 0.1, 24_000);
35
37
  const renderer = new THREE.WebGLRenderer({
36
38
  antialias: true,
37
39
  alpha: true,
@@ -41,27 +43,40 @@ export function createViewer3DRuntime(container) {
41
43
  renderer.domElement.dataset.worldorbit3dCanvas = "true";
42
44
  root.innerHTML = "";
43
45
  root.append(renderer.domElement);
44
- const ambientLight = new THREE.AmbientLight(0xffffff, 1.2);
45
- const keyLight = new THREE.PointLight(0xffffff, 1.35, 0, 2);
46
- scene3d.add(ambientLight);
47
- scene3d.add(keyLight);
46
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.24);
47
+ const fillLight = new THREE.DirectionalLight(0xcfe9ff, 0.36);
48
+ const rimLight = new THREE.DirectionalLight(0x7eb8ff, 0.24);
49
+ const keyLight = new THREE.PointLight(0xfff0cf, 2.6, 0, 2);
50
+ fillLight.position.set(-360, 260, 220);
51
+ rimLight.position.set(340, 180, -280);
48
52
  const orbitLayer = new THREE.Group();
49
53
  const objectLayer = new THREE.Group();
54
+ const starfield = createStarfield(THREE, 320);
55
+ const raycaster = new THREE.Raycaster();
56
+ raycaster.params.Line = { threshold: 7 };
57
+ scene3d.add(ambientLight);
58
+ scene3d.add(fillLight);
59
+ scene3d.add(rimLight);
60
+ scene3d.add(keyLight);
61
+ scene3d.add(starfield);
50
62
  scene3d.add(orbitLayer);
51
63
  scene3d.add(objectLayer);
52
- const raycaster = new THREE.Raycaster();
53
- raycaster.params.Line = { threshold: 10 };
54
64
  runtime = {
55
65
  THREE,
56
66
  scene3d,
57
67
  camera,
58
68
  renderer,
69
+ ambientLight,
70
+ fillLight,
71
+ rimLight,
59
72
  keyLight,
73
+ starfield,
60
74
  orbitLayer,
61
75
  objectLayer,
62
76
  raycaster,
63
77
  pointer: new THREE.Vector2(),
64
78
  };
79
+ configureRenderer(renderer, THREE, "balanced");
65
80
  if (pendingUpdate) {
66
81
  applyUpdate(pendingUpdate);
67
82
  }
@@ -113,18 +128,16 @@ export function createViewer3DRuntime(container) {
113
128
  const rect = runtime.renderer.domElement.getBoundingClientRect();
114
129
  const containerRect = container.getBoundingClientRect();
115
130
  return {
116
- x: rect.left -
117
- containerRect.left +
118
- ((vector.x + 1) / 2) * rect.width,
119
- y: rect.top -
120
- containerRect.top +
121
- ((1 - vector.y) / 2) * rect.height,
131
+ x: rect.left - containerRect.left + ((vector.x + 1) / 2) * rect.width,
132
+ y: rect.top - containerRect.top + ((1 - vector.y) / 2) * rect.height,
122
133
  };
123
134
  },
124
135
  destroy() {
125
136
  destroyed = true;
126
137
  pendingUpdate = null;
127
138
  runtime?.renderer.dispose();
139
+ runtime?.starfield?.geometry?.dispose?.();
140
+ runtime?.starfield?.material?.dispose?.();
128
141
  root.remove();
129
142
  objectVisuals.clear();
130
143
  orbitVisuals.clear();
@@ -143,7 +156,16 @@ export function createViewer3DRuntime(container) {
143
156
  currentVisibleObjectIds = next.visibleObjectIds;
144
157
  currentSelectedObjectId = next.selectedObjectId;
145
158
  currentHoveredObjectId = next.hoveredObjectId;
146
- currentTimeSeconds = next.timeSeconds;
159
+ configureRenderer(runtime.renderer, runtime.THREE, currentRenderOptions?.quality ?? "balanced");
160
+ const nextEnvironmentKey = JSON.stringify({
161
+ theme: currentRenderOptions?.theme ?? null,
162
+ quality: currentRenderOptions?.quality ?? "balanced",
163
+ style3d: currentRenderOptions?.style3d ?? "symbolic",
164
+ });
165
+ if (nextEnvironmentKey !== currentEnvironmentKey) {
166
+ updateEnvironment(runtime, currentRenderOptions);
167
+ currentEnvironmentKey = nextEnvironmentKey;
168
+ }
147
169
  if (sceneChanged) {
148
170
  rebuildScene(next.spatialScene);
149
171
  }
@@ -153,8 +175,9 @@ export function createViewer3DRuntime(container) {
153
175
  updateOrbitTransforms();
154
176
  updateVisibility();
155
177
  updateInteractionState();
178
+ updateLighting();
156
179
  updateCamera();
157
- renderNow();
180
+ runtime.renderer.render(runtime.scene3d, runtime.camera);
158
181
  }
159
182
  function rebuildScene(spatialScene) {
160
183
  if (!runtime) {
@@ -165,8 +188,9 @@ export function createViewer3DRuntime(container) {
165
188
  objectVisuals.clear();
166
189
  orbitVisuals.clear();
167
190
  raycastTargets.length = 0;
191
+ smoothedCameraPosition = null;
192
+ smoothedCameraTarget = null;
168
193
  const theme = resolveTheme(currentRenderOptions?.theme);
169
- runtime.scene3d.background = new runtime.THREE.Color(theme.backgroundStart);
170
194
  for (const orbit of spatialScene.orbits) {
171
195
  const visual = createOrbitVisual(runtime.THREE, orbit, theme);
172
196
  runtime.orbitLayer.add(visual.root);
@@ -184,10 +208,9 @@ export function createViewer3DRuntime(container) {
184
208
  for (const object of currentScene?.objects ?? []) {
185
209
  const visual = objectVisuals.get(object.objectId);
186
210
  const position = currentPositions.get(object.objectId);
187
- if (!visual || !position) {
188
- continue;
211
+ if (visual && position) {
212
+ visual.root.position.set(position.x, position.y, position.z);
189
213
  }
190
- visual.root.position.set(position.x, position.y, position.z);
191
214
  }
192
215
  }
193
216
  function updateOrbitTransforms() {
@@ -209,12 +232,11 @@ export function createViewer3DRuntime(container) {
209
232
  }
210
233
  const hideStructure = layers.structures === false &&
211
234
  (object.object.type === "structure" || object.object.type === "phenomenon");
212
- const hideObjects = layers.objects === false;
213
235
  visual.root.visible =
214
236
  !object.hidden &&
215
237
  currentVisibleObjectIds.has(object.objectId) &&
216
- !hideStructure &&
217
- !hideObjects;
238
+ layers.objects !== false &&
239
+ !hideStructure;
218
240
  }
219
241
  for (const orbit of currentScene?.orbits ?? []) {
220
242
  const visual = orbitVisuals.get(orbit.objectId);
@@ -235,18 +257,30 @@ export function createViewer3DRuntime(container) {
235
257
  return;
236
258
  }
237
259
  for (const visual of objectVisuals.values()) {
238
- applyVisualState(runtime.THREE, visual.materials, visual.baseColor, currentSelectedObjectId === visual.objectId, currentHoveredObjectId === visual.objectId);
239
- const scale = currentSelectedObjectId === visual.objectId
240
- ? 1.2
241
- : currentHoveredObjectId === visual.objectId
242
- ? 1.1
243
- : 1;
260
+ const selected = currentSelectedObjectId === visual.objectId;
261
+ const hovered = currentHoveredObjectId === visual.objectId;
262
+ applyVisualState(runtime.THREE, visual.materials, selected, hovered);
263
+ const scale = selected ? 1.16 : hovered ? 1.08 : 1;
244
264
  visual.root.scale.set(scale, scale, scale);
265
+ if (visual.halo) {
266
+ visual.halo.visible = selected || hovered;
267
+ }
245
268
  }
246
269
  for (const visual of orbitVisuals.values()) {
247
- applyVisualState(runtime.THREE, visual.materials, visual.baseColor, currentSelectedObjectId === visual.objectId, currentHoveredObjectId === visual.objectId);
270
+ applyVisualState(runtime.THREE, visual.materials, currentSelectedObjectId === visual.objectId, currentHoveredObjectId === visual.objectId);
248
271
  }
249
272
  }
273
+ function updateLighting() {
274
+ if (!runtime || !currentScene) {
275
+ return;
276
+ }
277
+ const primaryStar = currentScene.objects.find((object) => object.object.type === "star" && !object.hidden) ??
278
+ null;
279
+ const starPosition = primaryStar
280
+ ? currentPositions.get(primaryStar.objectId) ?? primaryStar.position
281
+ : { x: 0, y: 40, z: 0 };
282
+ runtime.keyLight.position.set(starPosition.x, starPosition.y + 20, starPosition.z);
283
+ }
250
284
  function updateCamera() {
251
285
  if (!runtime || !currentScene || !currentState) {
252
286
  return;
@@ -254,19 +288,28 @@ export function createViewer3DRuntime(container) {
254
288
  const sceneCamera = currentRenderOptions?.camera ?? currentScene.camera;
255
289
  const bounds = currentScene.contentBounds;
256
290
  const size = Math.max(bounds.width, bounds.depth, bounds.height, 160);
257
- const yaw = degreesToRadians((sceneCamera?.azimuth ?? 34) + currentState.rotationDeg);
258
- const pitch = degreesToRadians(clampValue(sceneCamera?.elevation ?? 24, -75, 75));
259
- const zoomDistanceFactor = clampValue(2.4 / Math.max(currentState.scale, 0.1), 0.35, 8);
260
- const semanticDistance = clampValue(sceneCamera?.distance ?? 6, 2, 24);
261
- const distance = clampValue(size * zoomDistanceFactor * (semanticDistance / 6), 28, 8_000);
291
+ const yaw = degreesToRadians((sceneCamera?.azimuth ?? 30) + currentState.rotationDeg);
292
+ const pitch = degreesToRadians(clampValue(sceneCamera?.elevation ?? 22, -75, 75));
293
+ const zoomDistanceFactor = clampValue(2.2 / Math.max(currentState.scale, 0.1), 0.35, 7.2);
294
+ const semanticDistance = clampValue(sceneCamera?.distance ?? 5.4, 2, 24);
295
+ const distance = clampValue(size * zoomDistanceFactor * (semanticDistance / 5.4), 24, 8_000);
262
296
  const panFactor = Math.max(size / 900, 0.12);
263
297
  const target = new runtime.THREE.Vector3(bounds.center.x - currentState.translateX * panFactor, bounds.center.y, bounds.center.z - currentState.translateY * panFactor);
264
- runtime.camera.position.set(target.x + distance * Math.cos(pitch) * Math.sin(yaw), target.y + distance * Math.sin(pitch), target.z + distance * Math.cos(pitch) * Math.cos(yaw));
265
- runtime.camera.lookAt(target);
298
+ const desiredPosition = new runtime.THREE.Vector3(target.x + distance * Math.cos(pitch) * Math.sin(yaw), target.y + distance * Math.sin(pitch), target.z + distance * Math.cos(pitch) * Math.cos(yaw));
299
+ const smoothing = (currentRenderOptions?.style3d ?? "symbolic") === "cinematic" ? 0.16 : 0.32;
300
+ if (!smoothedCameraPosition || !smoothedCameraTarget) {
301
+ smoothedCameraPosition = desiredPosition.clone();
302
+ smoothedCameraTarget = target.clone();
303
+ }
304
+ else {
305
+ smoothedCameraPosition.lerp(desiredPosition, smoothing);
306
+ smoothedCameraTarget.lerp(target, smoothing);
307
+ }
308
+ runtime.camera.position.copy(smoothedCameraPosition);
309
+ runtime.camera.lookAt(smoothedCameraTarget);
266
310
  if (sceneCamera?.roll) {
267
311
  runtime.camera.rotation.z = degreesToRadians(sceneCamera.roll);
268
312
  }
269
- runtime.keyLight.position.copy(runtime.camera.position);
270
313
  }
271
314
  function resizeRenderer(spatialScene) {
272
315
  if (!runtime) {
@@ -278,37 +321,63 @@ export function createViewer3DRuntime(container) {
278
321
  runtime.camera.aspect = width / height;
279
322
  runtime.camera.updateProjectionMatrix();
280
323
  }
281
- function renderNow() {
282
- if (!runtime) {
283
- return;
284
- }
285
- runtime.renderer.render(runtime.scene3d, runtime.camera);
286
- }
287
324
  }
288
325
  function createObjectVisual(THREE, object, theme) {
289
326
  const root = new THREE.Group();
290
327
  root.userData.objectId = object.objectId;
291
328
  const baseColor = object.fillColor ?? colorForObject(object);
292
- const material = new THREE.MeshPhongMaterial({
293
- color: baseColor,
294
- emissive: object.object.type === "star"
295
- ? new THREE.Color(theme.starGlow)
296
- : new THREE.Color(0x000000),
297
- emissiveIntensity: object.object.type === "star" ? 0.6 : 0.08,
298
- transparent: true,
299
- opacity: object.object.type === "phenomenon" ? 0.7 : 1,
300
- });
301
- const geometry = geometryForObject(THREE, object);
302
- const body = new THREE.Mesh(geometry, material);
329
+ const materials = [];
330
+ const bodyMaterial = materialForObject(THREE, object, baseColor, theme);
331
+ const body = new THREE.Mesh(geometryForObject(THREE, object), bodyMaterial.material);
303
332
  body.userData.objectId = object.objectId;
304
333
  root.add(body);
305
- return {
306
- objectId: object.objectId,
307
- root,
308
- body,
309
- materials: [material],
310
- baseColor,
311
- };
334
+ materials.push(bodyMaterial);
335
+ if (shouldRenderAtmosphere(object)) {
336
+ const atmosphereMaterial = {
337
+ material: new THREE.MeshBasicMaterial({
338
+ color: theme.atmosphere,
339
+ transparent: true,
340
+ opacity: 0.24,
341
+ depthWrite: false,
342
+ side: 2,
343
+ }),
344
+ baseColor: theme.atmosphere,
345
+ baseOpacity: 0.24,
346
+ hoveredOpacity: 0.34,
347
+ selectedOpacity: 0.42,
348
+ };
349
+ const atmosphere = new THREE.Mesh(new THREE.SphereGeometry(Math.max(object.visualRadius, 2) * 1.16, 20, 14), atmosphereMaterial.material);
350
+ atmosphere.userData.objectId = object.objectId;
351
+ root.add(atmosphere);
352
+ materials.push(atmosphereMaterial);
353
+ }
354
+ if (object.object.type === "comet") {
355
+ const tailMaterial = {
356
+ material: new THREE.MeshBasicMaterial({
357
+ color: theme.cometTail,
358
+ transparent: true,
359
+ opacity: 0.36,
360
+ depthWrite: false,
361
+ }),
362
+ baseColor: theme.cometTail,
363
+ baseOpacity: 0.36,
364
+ hoveredOpacity: 0.48,
365
+ selectedOpacity: 0.56,
366
+ };
367
+ const tail = new THREE.Mesh(new THREE.ConeGeometry(Math.max(object.visualRadius * 0.55, 2), Math.max(object.visualRadius * 2.8, 8), 12, 1, true), tailMaterial.material);
368
+ tail.position.set(-Math.max(object.visualRadius * 1.4, 4), 0, 0);
369
+ tail.rotation.z = -Math.PI / 2;
370
+ tail.userData.objectId = object.objectId;
371
+ root.add(tail);
372
+ materials.push(tailMaterial);
373
+ }
374
+ const halo = createHalo(THREE, object, theme);
375
+ if (halo) {
376
+ halo.visible = false;
377
+ halo.userData.objectId = object.objectId;
378
+ root.add(halo);
379
+ }
380
+ return { objectId: object.objectId, root, halo, materials };
312
381
  }
313
382
  function createOrbitVisual(THREE, orbit, theme) {
314
383
  const root = new THREE.Group();
@@ -316,61 +385,179 @@ function createOrbitVisual(THREE, orbit, theme) {
316
385
  root.rotation.y = degreesToRadians(orbit.rotationDeg);
317
386
  root.rotation.x = degreesToRadians(orbit.inclinationDeg);
318
387
  const baseColor = orbit.object.properties.color ?? theme.orbit;
319
- const materials = [];
320
388
  if (orbit.band) {
321
- const material = new THREE.MeshBasicMaterial({
322
- color: baseColor,
323
- transparent: true,
324
- opacity: 0.42,
325
- side: 2,
326
- });
327
- const geometry = bandGeometryForOrbit(THREE, orbit);
328
- const mesh = new THREE.Mesh(geometry, material);
389
+ const material = {
390
+ material: new THREE.MeshBasicMaterial({
391
+ color: baseColor,
392
+ transparent: true,
393
+ opacity: theme.orbitBandOpacity,
394
+ side: 2,
395
+ depthWrite: false,
396
+ }),
397
+ baseColor,
398
+ baseOpacity: theme.orbitBandOpacity,
399
+ hoveredOpacity: Math.min(theme.orbitBandOpacity + 0.1, 0.58),
400
+ selectedOpacity: Math.min(theme.orbitBandOpacity + 0.18, 0.72),
401
+ hoveredColor: theme.accent,
402
+ selectedColor: theme.accentStrong,
403
+ };
404
+ const mesh = new THREE.Mesh(bandGeometryForOrbit(THREE, orbit), material.material);
329
405
  mesh.userData.objectId = orbit.objectId;
330
406
  root.add(mesh);
331
- materials.push(material);
407
+ return { objectId: orbit.objectId, root, materials: [material] };
332
408
  }
333
- else {
334
- const material = new THREE.LineBasicMaterial({
409
+ const material = {
410
+ material: new THREE.LineBasicMaterial({
335
411
  color: baseColor,
336
412
  transparent: true,
337
- opacity: 0.55,
338
- });
339
- const points = sampleOrbitPoints(THREE, orbit);
340
- const geometry = new THREE.BufferGeometry().setFromPoints(points);
341
- const line = new THREE.LineLoop(geometry, material);
342
- line.userData.objectId = orbit.objectId;
343
- root.add(line);
344
- materials.push(material);
413
+ opacity: theme.orbitOpacity,
414
+ }),
415
+ baseColor,
416
+ baseOpacity: theme.orbitOpacity,
417
+ hoveredOpacity: Math.min(theme.orbitOpacity + 0.18, 0.72),
418
+ selectedOpacity: Math.min(theme.orbitOpacity + 0.3, 0.88),
419
+ hoveredColor: theme.accent,
420
+ selectedColor: theme.accentStrong,
421
+ };
422
+ const geometry = new THREE.BufferGeometry().setFromPoints(sampleOrbitPoints(THREE, orbit, 120));
423
+ const line = new THREE.LineLoop(geometry, material.material);
424
+ line.userData.objectId = orbit.objectId;
425
+ root.add(line);
426
+ return { objectId: orbit.objectId, root, materials: [material] };
427
+ }
428
+ function materialForObject(THREE, object, baseColor, theme) {
429
+ if (object.object.type === "star") {
430
+ return {
431
+ material: new THREE.MeshStandardMaterial({
432
+ color: baseColor,
433
+ emissive: new THREE.Color(theme.starGlow),
434
+ emissiveIntensity: 1.2,
435
+ roughness: 0.35,
436
+ metalness: 0.02,
437
+ }),
438
+ baseColor,
439
+ baseOpacity: 1,
440
+ hoveredOpacity: 1,
441
+ selectedOpacity: 1,
442
+ hoveredColor: theme.starCore,
443
+ selectedColor: "#fff2c4",
444
+ baseEmissive: theme.starGlow,
445
+ hoveredEmissive: theme.starGlow,
446
+ selectedEmissive: "#fff6cc",
447
+ baseEmissiveIntensity: 1.2,
448
+ hoveredEmissiveIntensity: 1.5,
449
+ selectedEmissiveIntensity: 1.8,
450
+ };
345
451
  }
452
+ if (object.object.type === "phenomenon") {
453
+ return {
454
+ material: new THREE.MeshPhongMaterial({
455
+ color: baseColor,
456
+ transparent: true,
457
+ opacity: 0.7,
458
+ emissive: new THREE.Color(baseColor),
459
+ emissiveIntensity: 0.32,
460
+ shininess: 90,
461
+ }),
462
+ baseColor,
463
+ baseOpacity: 0.7,
464
+ hoveredOpacity: 0.82,
465
+ selectedOpacity: 0.9,
466
+ hoveredColor: theme.accent,
467
+ selectedColor: theme.selectionHalo,
468
+ baseEmissive: baseColor,
469
+ hoveredEmissive: theme.accent,
470
+ selectedEmissive: theme.selectionHalo,
471
+ baseEmissiveIntensity: 0.32,
472
+ hoveredEmissiveIntensity: 0.52,
473
+ selectedEmissiveIntensity: 0.74,
474
+ };
475
+ }
476
+ const shininess = object.object.type === "structure" ? 70 :
477
+ object.object.type === "ring" ? 42 :
478
+ object.object.type === "belt" ? 26 :
479
+ 36;
346
480
  return {
347
- objectId: orbit.objectId,
348
- root,
349
- materials,
481
+ material: new THREE.MeshPhongMaterial({
482
+ color: baseColor,
483
+ specular: new THREE.Color(theme.objectSpecular),
484
+ shininess,
485
+ transparent: false,
486
+ opacity: 1,
487
+ emissive: new THREE.Color(0x000000),
488
+ emissiveIntensity: 0.02,
489
+ }),
350
490
  baseColor,
491
+ baseOpacity: 1,
492
+ hoveredOpacity: 1,
493
+ selectedOpacity: 1,
494
+ hoveredColor: shiftColorLightness(THREE, baseColor, 0.08),
495
+ selectedColor: shiftColorLightness(THREE, baseColor, 0.16),
496
+ hoveredEmissive: "#8fcaff",
497
+ selectedEmissive: theme.selectionHalo,
498
+ baseEmissiveIntensity: 0.02,
499
+ hoveredEmissiveIntensity: 0.12,
500
+ selectedEmissiveIntensity: 0.22,
351
501
  };
352
502
  }
353
503
  function geometryForObject(THREE, object) {
354
504
  const radius = Math.max(object.visualRadius, 2);
355
505
  switch (object.object.type) {
356
506
  case "star":
357
- return new THREE.SphereGeometry(radius * 1.12, 28, 20);
507
+ return new THREE.SphereGeometry(radius * 1.14, 34, 24);
358
508
  case "structure":
359
- return new THREE.BoxGeometry(radius * 1.5, radius * 1.5, radius * 1.5);
509
+ return geometryForStructure(THREE, object, radius);
360
510
  case "phenomenon":
361
- return new THREE.OctahedronGeometry(radius * 1.25, 0);
511
+ return new THREE.IcosahedronGeometry(radius * 1.12, 1);
362
512
  case "belt":
513
+ return new THREE.TorusGeometry(Math.max(radius * 1.15, 4), Math.max(radius * 0.28, 1), 10, 24);
363
514
  case "ring":
364
- return new THREE.OctahedronGeometry(Math.max(radius * 0.85, 3), 0);
515
+ return new THREE.TorusGeometry(Math.max(radius, 4), Math.max(radius * 0.18, 0.8), 10, 30);
516
+ case "asteroid":
517
+ return new THREE.DodecahedronGeometry(radius, 0);
518
+ case "comet":
519
+ return new THREE.SphereGeometry(radius * 0.94, 18, 14);
365
520
  default:
366
- return new THREE.SphereGeometry(radius, 20, 14);
521
+ return new THREE.SphereGeometry(radius, 24, 18);
522
+ }
523
+ }
524
+ function geometryForStructure(THREE, object, radius) {
525
+ const kind = String(object.object.properties.kind ?? "").toLowerCase();
526
+ if (kind.includes("relay")) {
527
+ return new THREE.OctahedronGeometry(radius * 1.15, 0);
528
+ }
529
+ if (kind.includes("elevator") || kind.includes("skyhook")) {
530
+ return new THREE.CylinderGeometry(radius * 0.36, radius * 0.52, radius * 2.4, 10);
531
+ }
532
+ if (kind.includes("station")) {
533
+ return new THREE.TorusKnotGeometry(radius * 0.6, Math.max(radius * 0.18, 0.6), 42, 8);
534
+ }
535
+ return new THREE.BoxGeometry(radius * 1.45, radius * 1.2, radius * 1.45);
536
+ }
537
+ function createHalo(THREE, object, theme) {
538
+ const radius = Math.max(object.visualRadius, 2);
539
+ const geometry = object.object.type === "structure"
540
+ ? new THREE.BoxGeometry(radius * 2.2, radius * 2.2, radius * 2.2)
541
+ : new THREE.SphereGeometry(radius * 1.38, 18, 14);
542
+ return new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({
543
+ color: theme.selectionHalo,
544
+ transparent: true,
545
+ opacity: 0.18,
546
+ depthWrite: false,
547
+ side: 1,
548
+ }));
549
+ }
550
+ function shouldRenderAtmosphere(object) {
551
+ if (object.object.type !== "planet" && object.object.type !== "moon") {
552
+ return false;
367
553
  }
554
+ return object.object.properties.atmosphere !== undefined;
368
555
  }
369
556
  function bandGeometryForOrbit(THREE, orbit) {
370
557
  const thickness = Math.max(orbit.bandThickness ?? 8, 3);
371
558
  const points = sampleOrbitPoints(THREE, orbit, 72);
372
559
  const curve = new THREE.CatmullRomCurve3(points, true);
373
- return new THREE.TubeGeometry(curve, 128, thickness * 0.28, 10, true);
560
+ return new THREE.TubeGeometry(curve, 144, thickness * 0.18, 10, true);
374
561
  }
375
562
  function sampleOrbitPoints(THREE, orbit, segments = 96) {
376
563
  const points = [];
@@ -382,6 +569,111 @@ function sampleOrbitPoints(THREE, orbit, segments = 96) {
382
569
  }
383
570
  return points;
384
571
  }
572
+ function createStarfield(THREE, count) {
573
+ const geometry = new THREE.BufferGeometry();
574
+ const positions = new Float32Array(count * 3);
575
+ const colors = new Float32Array(count * 3);
576
+ for (let index = 0; index < count; index += 1) {
577
+ const offset = index * 3;
578
+ const radius = 1800 + Math.random() * 2600;
579
+ const theta = Math.random() * Math.PI * 2;
580
+ const phi = Math.acos(2 * Math.random() - 1);
581
+ positions[offset] = radius * Math.sin(phi) * Math.cos(theta);
582
+ positions[offset + 1] = radius * Math.cos(phi) * 0.45;
583
+ positions[offset + 2] = radius * Math.sin(phi) * Math.sin(theta);
584
+ }
585
+ geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
586
+ geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
587
+ return new THREE.Points(geometry, new THREE.PointsMaterial({
588
+ size: 5,
589
+ transparent: true,
590
+ opacity: 0.84,
591
+ depthWrite: false,
592
+ vertexColors: true,
593
+ sizeAttenuation: true,
594
+ }));
595
+ }
596
+ function configureRenderer(renderer, THREE, quality) {
597
+ const pixelRatioCap = quality === "high" ? 2.4 : quality === "low" ? 1.2 : 1.8;
598
+ renderer.setPixelRatio?.(Math.min(globalThis.window?.devicePixelRatio ?? 1, pixelRatioCap));
599
+ if ("outputColorSpace" in renderer && "SRGBColorSpace" in THREE) {
600
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
601
+ }
602
+ else if ("outputEncoding" in renderer && "sRGBEncoding" in THREE) {
603
+ renderer.outputEncoding = THREE.sRGBEncoding;
604
+ }
605
+ if ("ACESFilmicToneMapping" in THREE) {
606
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
607
+ }
608
+ }
609
+ function updateEnvironment(runtime, renderOptions) {
610
+ const theme = resolveTheme(renderOptions?.theme);
611
+ const quality = renderOptions?.quality ?? "balanced";
612
+ const style3d = renderOptions?.style3d ?? "symbolic";
613
+ const count = quality === "high" ? 520 : quality === "low" ? 180 : 320;
614
+ const positions = new Float32Array(count * 3);
615
+ const colors = new Float32Array(count * 3);
616
+ const bright = new runtime.THREE.Color(theme.starfield);
617
+ const dim = new runtime.THREE.Color(theme.starfieldDim);
618
+ for (let index = 0; index < count; index += 1) {
619
+ const offset = index * 3;
620
+ const radius = 1800 + Math.random() * 2600;
621
+ const theta = Math.random() * Math.PI * 2;
622
+ const phi = Math.acos(2 * Math.random() - 1);
623
+ positions[offset] = radius * Math.sin(phi) * Math.cos(theta);
624
+ positions[offset + 1] = radius * Math.cos(phi) * 0.45;
625
+ positions[offset + 2] = radius * Math.sin(phi) * Math.sin(theta);
626
+ const color = Math.random() > 0.72 ? bright : dim;
627
+ colors[offset] = color.r;
628
+ colors[offset + 1] = color.g;
629
+ colors[offset + 2] = color.b;
630
+ }
631
+ runtime.scene3d.background = new runtime.THREE.Color(theme.backgroundStart);
632
+ runtime.scene3d.fog = new runtime.THREE.FogExp2(theme.spaceFog, 0.00085);
633
+ runtime.ambientLight.intensity = style3d === "cinematic" ? 0.18 : 0.26;
634
+ runtime.fillLight.intensity = style3d === "cinematic" ? 0.32 : 0.4;
635
+ runtime.rimLight.intensity = style3d === "cinematic" ? 0.28 : 0.22;
636
+ runtime.renderer.toneMappingExposure = style3d === "cinematic" ? 1.18 : 1.08;
637
+ runtime.starfield.geometry.setAttribute("position", new runtime.THREE.BufferAttribute(positions, 3));
638
+ runtime.starfield.geometry.setAttribute("color", new runtime.THREE.BufferAttribute(colors, 3));
639
+ runtime.starfield.material.opacity = quality === "high" ? 0.96 : quality === "low" ? 0.72 : 0.84;
640
+ runtime.starfield.material.size = quality === "high" ? 5.5 : quality === "low" ? 4.25 : 5;
641
+ }
642
+ function applyVisualState(THREE, materials, selected, hovered) {
643
+ for (const entry of materials) {
644
+ const material = entry.material;
645
+ if (!material) {
646
+ continue;
647
+ }
648
+ const color = selected
649
+ ? entry.selectedColor ?? entry.baseColor
650
+ : hovered
651
+ ? entry.hoveredColor ?? entry.baseColor
652
+ : entry.baseColor;
653
+ material.color?.set?.(new THREE.Color(color));
654
+ if (typeof material.opacity === "number") {
655
+ material.opacity = selected
656
+ ? entry.selectedOpacity
657
+ : hovered
658
+ ? entry.hoveredOpacity
659
+ : entry.baseOpacity;
660
+ material.transparent = material.opacity < 0.999;
661
+ }
662
+ const emissive = selected
663
+ ? entry.selectedEmissive ?? entry.baseEmissive
664
+ : hovered
665
+ ? entry.hoveredEmissive ?? entry.baseEmissive
666
+ : entry.baseEmissive;
667
+ material.emissive?.set?.(emissive ? new THREE.Color(emissive) : new THREE.Color(0x000000));
668
+ if ("emissiveIntensity" in material) {
669
+ material.emissiveIntensity = selected
670
+ ? entry.selectedEmissiveIntensity ?? entry.baseEmissiveIntensity ?? 0
671
+ : hovered
672
+ ? entry.hoveredEmissiveIntensity ?? entry.baseEmissiveIntensity ?? 0
673
+ : entry.baseEmissiveIntensity ?? 0;
674
+ }
675
+ }
676
+ }
385
677
  function colorForObject(object) {
386
678
  switch (object.object.type) {
387
679
  case "star":
@@ -404,37 +696,24 @@ function colorForObject(object) {
404
696
  return "#b8f2ff";
405
697
  }
406
698
  }
407
- function applyVisualState(THREE, materials, baseColor, selected, hovered) {
408
- const color = new THREE.Color(baseColor);
409
- if (selected) {
410
- color.offsetHSL(0, 0, 0.16);
411
- }
412
- else if (hovered) {
413
- color.offsetHSL(0, 0, 0.08);
414
- }
415
- for (const material of materials) {
416
- if (!material) {
417
- continue;
418
- }
419
- material.color?.set?.(color);
420
- if (typeof material.opacity === "number") {
421
- material.opacity = selected ? 0.85 : hovered ? 0.72 : material.transparent ? 0.55 : 1;
422
- }
423
- material.emissive?.set?.(selected ? new THREE.Color("#ffdda9") : hovered ? new THREE.Color("#cfe9ff") : new THREE.Color(0x000000));
424
- material.emissiveIntensity = selected ? 0.28 : hovered ? 0.14 : material.emissiveIntensity ?? 0.08;
425
- }
699
+ function shiftColorLightness(THREE, colorValue, delta) {
700
+ const color = new THREE.Color(colorValue);
701
+ color.offsetHSL(0, 0, delta);
702
+ return `#${color.getHexString()}`;
426
703
  }
427
704
  function clearGroup(group) {
428
705
  while (group.children.length > 0) {
429
706
  const child = group.children[0];
430
707
  group.remove(child);
431
- child.geometry?.dispose?.();
432
- if (Array.isArray(child.material)) {
433
- child.material.forEach((entry) => entry?.dispose?.());
434
- }
435
- else {
436
- child.material?.dispose?.();
437
- }
708
+ child.traverse?.((node) => {
709
+ node.geometry?.dispose?.();
710
+ if (Array.isArray(node.material)) {
711
+ node.material.forEach((entry) => entry?.dispose?.());
712
+ }
713
+ else {
714
+ node.material?.dispose?.();
715
+ }
716
+ });
438
717
  }
439
718
  }
440
719
  function ensureWebGLSupport() {