worldorbit 3.0.5 → 3.0.7
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/README.md +2 -2
- package/dist/browser/viewer/dist/embed.js +86 -13
- package/dist/browser/viewer/dist/runtime-3d.js +396 -117
- package/dist/browser/viewer/dist/theme.js +27 -0
- package/dist/browser/viewer/dist/types.d.ts +17 -0
- package/dist/browser/viewer/dist/viewer.js +268 -12
- package/dist/unpkg/viewer/dist/embed.js +86 -13
- package/dist/unpkg/viewer/dist/runtime-3d.js +396 -117
- package/dist/unpkg/viewer/dist/theme.js +27 -0
- package/dist/unpkg/viewer/dist/types.d.ts +17 -0
- package/dist/unpkg/viewer/dist/viewer.js +268 -12
- package/dist/unpkg/worldorbit-editor.min.js +368 -314
- package/dist/unpkg/worldorbit-markdown.min.js +13 -13
- package/dist/unpkg/worldorbit-viewer.min.js +272 -218
- package/dist/unpkg/worldorbit.js +735 -127
- package/dist/unpkg/worldorbit.min.js +265 -211
- package/package.json +1 -1
- package/packages/viewer/dist/embed.js +86 -13
- package/packages/viewer/dist/runtime-3d.js +396 -117
- package/packages/viewer/dist/theme.js +27 -0
- package/packages/viewer/dist/types.d.ts +17 -0
- package/packages/viewer/dist/viewer.js +268 -12
|
@@ -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(
|
|
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,
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
188
|
-
|
|
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
|
-
|
|
217
|
-
!
|
|
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
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
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,
|
|
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 ??
|
|
258
|
-
const pitch = degreesToRadians(clampValue(sceneCamera?.elevation ??
|
|
259
|
-
const zoomDistanceFactor = clampValue(2.
|
|
260
|
-
const semanticDistance = clampValue(sceneCamera?.distance ??
|
|
261
|
-
const distance = clampValue(size * zoomDistanceFactor * (semanticDistance /
|
|
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.
|
|
265
|
-
|
|
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
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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 =
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
|
407
|
+
return { objectId: orbit.objectId, root, materials: [material] };
|
|
332
408
|
}
|
|
333
|
-
|
|
334
|
-
|
|
409
|
+
const material = {
|
|
410
|
+
material: new THREE.LineBasicMaterial({
|
|
335
411
|
color: baseColor,
|
|
336
412
|
transparent: true,
|
|
337
|
-
opacity:
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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.
|
|
507
|
+
return new THREE.SphereGeometry(radius * 1.14, 34, 24);
|
|
358
508
|
case "structure":
|
|
359
|
-
return
|
|
509
|
+
return geometryForStructure(THREE, object, radius);
|
|
360
510
|
case "phenomenon":
|
|
361
|
-
return new THREE.
|
|
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.
|
|
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,
|
|
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,
|
|
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
|
|
408
|
-
const color = new THREE.Color(
|
|
409
|
-
|
|
410
|
-
|
|
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.
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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() {
|