diff --git a/public/images/3DModels/new_horizons.glb b/public/images/3DModels/new_horizons.glb new file mode 100644 index 0000000..2012b33 Binary files /dev/null and b/public/images/3DModels/new_horizons.glb differ diff --git a/public/images/3DModels/pioneer10.glb b/public/images/3DModels/pioneer10.glb new file mode 100644 index 0000000..62d2608 Binary files /dev/null and b/public/images/3DModels/pioneer10.glb differ diff --git a/public/images/3DModels/voyager1.glb b/public/images/3DModels/voyager1.glb new file mode 100644 index 0000000..bb997a2 Binary files /dev/null and b/public/images/3DModels/voyager1.glb differ diff --git a/public/images/3DModels/voyager2.glb b/public/images/3DModels/voyager2.glb new file mode 100644 index 0000000..bb997a2 Binary files /dev/null and b/public/images/3DModels/voyager2.glb differ diff --git a/public/solarsystem.php b/public/solarsystem.php index 6e72b22..61e5b3a 100644 --- a/public/solarsystem.php +++ b/public/solarsystem.php @@ -591,8 +591,9 @@ let newHorizonsObjects = null; let newHorizonsLabelElement = null; let pioneer10Objects = null; let pioneer10LabelElement = null; -let voyagerModelTemplatePromise = null; -let voyagerModelTemplate = null; +const spacecraftModelTemplatePromises = new Map(); +const spacecraftModelTemplates = new Map(); +const spacecraftReferenceDirections = new Map(); const planetDynamicsCache = new Map(); let showOrbits = true; @@ -660,24 +661,102 @@ function updateSpeedFromSlider() { speedLabel.textContent = normalizedSpeedText(speedDaysPerSecond); } -async function loadVoyagerModelTemplate() { - if (voyagerModelTemplatePromise) { - return voyagerModelTemplatePromise; +const SPACECRAFT_CONFIGS = { + voyager1: { + key: 'voyager1', + path: './images/3DModels/voyager1.glb', + label: 'Voyager 1', + infoColor: '#8fe3ff', + orbitColor: '#8fe3ff', + meshColor: '#b7f0ff', + meshEmissive: '#4db5d4', + modelScale: 0.16, + rotationDeg: { x: 0, y: 71, z: 90 }, + orientationMode: 'look_at_earth', + orientationReferenceIso: '1977-09-05T12:00:00Z', + }, + voyager2: { + key: 'voyager2', + path: './images/3DModels/voyager2.glb', + label: 'Voyager 2', + infoColor: '#ffd36f', + orbitColor: '#ffd36f', + meshColor: '#ffe4a6', + meshEmissive: '#d4972e', + modelScale: 0.16, + rotationDeg: { x: 9, y: 54, z: 83 }, + orientationMode: 'look_at_earth', + orientationReferenceIso: '1977-08-20T12:00:00Z', + }, + new_horizons: { + key: 'new_horizons', + path: './images/3DModels/new_horizons.glb', + label: 'New Horizons', + infoColor: '#ff9fd6', + orbitColor: '#ff9fd6', + meshColor: '#ffd4ec', + meshEmissive: '#cc4da0', + targetSize: 0.72, + rotationDeg: { x: 0, y: 25, z: -90 }, + orientationMode: 'look_at_earth', + orientationReferenceIso: '2006-01-19T12:00:00Z', + }, + pioneer10: { + key: 'pioneer10', + path: './images/3DModels/pioneer10.glb', + label: 'Pioneer 10', + infoColor: '#f3a86f', + orbitColor: '#f3a86f', + meshColor: '#f7c49c', + meshEmissive: '#bc6a2e', + targetSize: 0.72, + rotationDeg: { x: -3, y: -18, z: 0 }, + orientationMode: 'look_at_earth', + orientationReferenceIso: '1972-03-03T12:00:00Z', + }, +}; + +function getSpacecraftConfig(spacecraftKey) { + return SPACECRAFT_CONFIGS[String(spacecraftKey)] || null; +} + +function getRotationRadians(rotationDeg = {}) { + return { + x: THREE.MathUtils.degToRad(Number(rotationDeg.x || 0)), + y: THREE.MathUtils.degToRad(Number(rotationDeg.y || 0)), + z: THREE.MathUtils.degToRad(Number(rotationDeg.z || 0)), + }; +} + +function applyManualRotationToObject(object3D, rotationDeg = {}) { + if (!object3D) { + return; } - if (!GLTFLoaderClass) { + const rotationRadians = getRotationRadians(rotationDeg); + object3D.rotation.set(rotationRadians.x, rotationRadians.y, rotationRadians.z); +} + +async function loadSpacecraftModelTemplate(spacecraftKey) { + const config = getSpacecraftConfig(spacecraftKey); + if (!config || !GLTFLoaderClass) { return null; } + if (spacecraftModelTemplatePromises.has(spacecraftKey)) { + return spacecraftModelTemplatePromises.get(spacecraftKey); + } + const loader = new GLTFLoaderClass(); if (DRACOLoaderClass) { const dracoLoader = new DRACOLoaderClass(); dracoLoader.setDecoderPath('./js/libs/draco/'); loader.setDRACOLoader(dracoLoader); } - voyagerModelTemplatePromise = new Promise((resolve) => { + + const promise = new Promise((resolve) => { loader.load( - './images/3DModels/voyager.glb', + config.path, (gltf) => { const template = gltf.scene; template.traverse((node) => { @@ -687,22 +766,23 @@ async function loadVoyagerModelTemplate() { node.castShadow = false; node.receiveShadow = false; }); - voyagerModelTemplate = template; + spacecraftModelTemplates.set(spacecraftKey, template); resolve(template); }, undefined, (error) => { - console.warn('Voyager-Modell konnte nicht geladen werden, Fallback wird verwendet.', error); - voyagerModelTemplate = null; + console.warn(`${config.label}-Modell konnte nicht geladen werden, Fallback wird verwendet.`, error); + spacecraftModelTemplates.set(spacecraftKey, null); resolve(null); } ); }); - return voyagerModelTemplatePromise; + spacecraftModelTemplatePromises.set(spacecraftKey, promise); + return promise; } -function createVoyagerFallbackMesh(color, emissive) { +function createSpacecraftFallbackMesh(color, emissive) { const mesh = new THREE.Mesh( new THREE.SphereGeometry(0.28, 20, 20), new THREE.MeshStandardMaterial({ @@ -718,17 +798,72 @@ function createVoyagerFallbackMesh(color, emissive) { return mesh; } -function createVoyagerDisplayObject(color, emissive) { - if (voyagerModelTemplate) { - const model = voyagerModelTemplate.clone(true); - model.scale.setScalar(0.16); - model.rotation.x = Math.PI / 2; - model.userData.baseScale = 1; - model.userData.minScaleMultiplier = 1.8; +function createSpacecraftModelNode(spacecraftKey, fallbackNode = null) { + const config = getSpacecraftConfig(spacecraftKey); + const template = spacecraftModelTemplates.get(spacecraftKey) || null; + + if (!config) { + return fallbackNode || new THREE.Group(); + } + + if (template) { + const model = template.clone(true); + + if (config.targetSize) { + const bounds = new THREE.Box3().setFromObject(model); + const size = new THREE.Vector3(); + bounds.getSize(size); + const maxDimension = Math.max(size.x, size.y, size.z, 1e-6); + model.scale.setScalar(Number(config.targetSize) / maxDimension); + } else if (config.modelScale) { + model.scale.setScalar(Number(config.modelScale)); + } + return model; } - return createVoyagerFallbackMesh(color, emissive); + return fallbackNode || createSpacecraftFallbackMesh(config.meshColor, config.meshEmissive); +} + +function createSpacecraftDisplayObject(spacecraftKey) { + const config = getSpacecraftConfig(spacecraftKey); + if (!config) { + return null; + } + + const root = new THREE.Group(); + const orientationGroup = new THREE.Group(); + const modelGroup = new THREE.Group(); + const fallbackNode = createSpacecraftFallbackMesh(config.meshColor, config.meshEmissive); + const contentNode = createSpacecraftModelNode(spacecraftKey, fallbackNode); + + applyManualRotationToObject(modelGroup, config.rotationDeg); + + modelGroup.add(contentNode); + orientationGroup.add(modelGroup); + root.add(orientationGroup); + root.userData.baseScale = 1; + root.userData.minScaleMultiplier = 1.8; + root.userData.spacecraftKey = spacecraftKey; + root.userData.orientationGroup = orientationGroup; + root.userData.modelGroup = modelGroup; + root.userData.contentNode = contentNode; + root.userData.fallbackNode = fallbackNode; + + loadSpacecraftModelTemplate(spacecraftKey).then((template) => { + if (!template || !root.userData.modelGroup) { + return; + } + + const nextNode = createSpacecraftModelNode(spacecraftKey, root.userData.fallbackNode); + if (root.userData.contentNode && root.userData.contentNode.parent === root.userData.modelGroup) { + root.userData.modelGroup.remove(root.userData.contentNode); + } + root.userData.modelGroup.add(nextNode); + root.userData.contentNode = nextNode; + }); + + return root; } function adjustSpeedSliderFromWheel(event) { @@ -822,24 +957,40 @@ function getSelectedSpacecraftSet() { return new Set(selectedSpacecraftKeys); } -function getVoyager1Position(date) { - if (!Array.isArray(VOYAGER1_TRACK) || VOYAGER1_TRACK.length === 0) { +function getSpacecraftTrack(spacecraftKey) { + switch (String(spacecraftKey)) { + case 'voyager1': + return VOYAGER1_TRACK; + case 'voyager2': + return VOYAGER2_TRACK; + case 'new_horizons': + return NEW_HORIZONS_TRACK; + case 'pioneer10': + return PIONEER10_TRACK; + default: + return []; + } +} + +function getSpacecraftPositionAtDate(spacecraftKey, date) { + const track = getSpacecraftTrack(spacecraftKey); + if (!Array.isArray(track) || track.length === 0) { return null; } const targetMs = date.getTime(); - if (targetMs <= Number(VOYAGER1_TRACK[0].timestampMs)) { - return VOYAGER1_TRACK[0]; + if (targetMs <= Number(track[0].timestampMs)) { + return track[0]; } - const lastIndex = VOYAGER1_TRACK.length - 1; - if (targetMs >= Number(VOYAGER1_TRACK[lastIndex].timestampMs)) { - return VOYAGER1_TRACK[lastIndex]; + const lastIndex = track.length - 1; + if (targetMs >= Number(track[lastIndex].timestampMs)) { + return track[lastIndex]; } for (let index = 0; index < lastIndex; index += 1) { - const left = VOYAGER1_TRACK[index]; - const right = VOYAGER1_TRACK[index + 1]; + const left = track[index]; + const right = track[index + 1]; const leftMs = Number(left.timestampMs); const rightMs = Number(right.timestampMs); if (targetMs < leftMs || targetMs > rightMs) { @@ -866,136 +1017,20 @@ function getVoyager1Position(date) { return null; } +function getVoyager1Position(date) { + return getSpacecraftPositionAtDate('voyager1', date); +} + function getVoyager2Position(date) { - if (!Array.isArray(VOYAGER2_TRACK) || VOYAGER2_TRACK.length === 0) { - return null; - } - - const targetMs = date.getTime(); - if (targetMs <= Number(VOYAGER2_TRACK[0].timestampMs)) { - return VOYAGER2_TRACK[0]; - } - - const lastIndex = VOYAGER2_TRACK.length - 1; - if (targetMs >= Number(VOYAGER2_TRACK[lastIndex].timestampMs)) { - return VOYAGER2_TRACK[lastIndex]; - } - - for (let index = 0; index < lastIndex; index += 1) { - const left = VOYAGER2_TRACK[index]; - const right = VOYAGER2_TRACK[index + 1]; - const leftMs = Number(left.timestampMs); - const rightMs = Number(right.timestampMs); - if (targetMs < leftMs || targetMs > rightMs) { - continue; - } - - const span = Math.max(rightMs - leftMs, 1); - const factor = (targetMs - leftMs) / span; - const xAu = Number(left.xAu) + ((Number(right.xAu) - Number(left.xAu)) * factor); - const yAu = Number(left.yAu) + ((Number(right.yAu) - Number(left.yAu)) * factor); - const zAu = Number(left.zAu) + ((Number(right.zAu) - Number(left.zAu)) * factor); - const distanceAu = Math.hypot(xAu, yAu, zAu); - - return { - timestampMs: targetMs, - dateTdb: String(left.dateTdb || ''), - xAu, - yAu, - zAu, - distanceAu, - }; - } - - return null; + return getSpacecraftPositionAtDate('voyager2', date); } function getNewHorizonsPosition(date) { - if (!Array.isArray(NEW_HORIZONS_TRACK) || NEW_HORIZONS_TRACK.length === 0) { - return null; - } - - const targetMs = date.getTime(); - if (targetMs <= Number(NEW_HORIZONS_TRACK[0].timestampMs)) { - return NEW_HORIZONS_TRACK[0]; - } - - const lastIndex = NEW_HORIZONS_TRACK.length - 1; - if (targetMs >= Number(NEW_HORIZONS_TRACK[lastIndex].timestampMs)) { - return NEW_HORIZONS_TRACK[lastIndex]; - } - - for (let index = 0; index < lastIndex; index += 1) { - const left = NEW_HORIZONS_TRACK[index]; - const right = NEW_HORIZONS_TRACK[index + 1]; - const leftMs = Number(left.timestampMs); - const rightMs = Number(right.timestampMs); - if (targetMs < leftMs || targetMs > rightMs) { - continue; - } - - const span = Math.max(rightMs - leftMs, 1); - const factor = (targetMs - leftMs) / span; - const xAu = Number(left.xAu) + ((Number(right.xAu) - Number(left.xAu)) * factor); - const yAu = Number(left.yAu) + ((Number(right.yAu) - Number(left.yAu)) * factor); - const zAu = Number(left.zAu) + ((Number(right.zAu) - Number(left.zAu)) * factor); - const distanceAu = Math.hypot(xAu, yAu, zAu); - - return { - timestampMs: targetMs, - dateTdb: String(left.dateTdb || ''), - xAu, - yAu, - zAu, - distanceAu, - }; - } - - return null; + return getSpacecraftPositionAtDate('new_horizons', date); } function getPioneer10Position(date) { - if (!Array.isArray(PIONEER10_TRACK) || PIONEER10_TRACK.length === 0) { - return null; - } - - const targetMs = date.getTime(); - if (targetMs <= Number(PIONEER10_TRACK[0].timestampMs)) { - return PIONEER10_TRACK[0]; - } - - const lastIndex = PIONEER10_TRACK.length - 1; - if (targetMs >= Number(PIONEER10_TRACK[lastIndex].timestampMs)) { - return PIONEER10_TRACK[lastIndex]; - } - - for (let index = 0; index < lastIndex; index += 1) { - const left = PIONEER10_TRACK[index]; - const right = PIONEER10_TRACK[index + 1]; - const leftMs = Number(left.timestampMs); - const rightMs = Number(right.timestampMs); - if (targetMs < leftMs || targetMs > rightMs) { - continue; - } - - const span = Math.max(rightMs - leftMs, 1); - const factor = (targetMs - leftMs) / span; - const xAu = Number(left.xAu) + ((Number(right.xAu) - Number(left.xAu)) * factor); - const yAu = Number(left.yAu) + ((Number(right.yAu) - Number(left.yAu)) * factor); - const zAu = Number(left.zAu) + ((Number(right.zAu) - Number(left.zAu)) * factor); - const distanceAu = Math.hypot(xAu, yAu, zAu); - - return { - timestampMs: targetMs, - dateTdb: String(left.dateTdb || ''), - xAu, - yAu, - zAu, - distanceAu, - }; - } - - return null; + return getSpacecraftPositionAtDate('pioneer10', date); } const MINOR_PLANET_COLOR_PALETTE = [ @@ -1162,6 +1197,81 @@ function toDisplayDirection(vector) { ); } +function getSpacecraftReferenceEarthDirection(spacecraftKey) { + if (spacecraftReferenceDirections.has(spacecraftKey)) { + return spacecraftReferenceDirections.get(spacecraftKey); + } + + const config = getSpacecraftConfig(spacecraftKey); + if (!config || config.orientationMode !== 'look_at_earth' || !config.orientationReferenceIso) { + spacecraftReferenceDirections.set(spacecraftKey, null); + return null; + } + + const referenceDate = new Date(config.orientationReferenceIso); + const spacecraftPosition = getSpacecraftPositionAtDate(spacecraftKey, referenceDate); + if (!spacecraftPosition) { + spacecraftReferenceDirections.set(spacecraftKey, null); + return null; + } + + const earthPosition = getHelioPosition('Earth', referenceDate); + const earthVector = { + x: Number(earthPosition.x) - Number(spacecraftPosition.xAu), + y: Number(earthPosition.y) - Number(spacecraftPosition.yAu), + z: Number(earthPosition.z) - Number(spacecraftPosition.zAu), + }; + const direction = toDisplayDirection(earthVector); + + if (!Number.isFinite(direction.lengthSq()) || direction.lengthSq() < 1e-12) { + spacecraftReferenceDirections.set(spacecraftKey, null); + return null; + } + + direction.normalize(); + spacecraftReferenceDirections.set(spacecraftKey, direction.clone()); + return direction; +} + +function getSpacecraftCurrentEarthDirection(heliocentricPosition) { + const earthPosition = getHelioPosition('Earth', simDate); + const earthVector = { + x: Number(earthPosition.x) - Number(heliocentricPosition.xAu), + y: Number(earthPosition.y) - Number(heliocentricPosition.yAu), + z: Number(earthPosition.z) - Number(heliocentricPosition.zAu), + }; + const direction = toDisplayDirection(earthVector); + + if (!Number.isFinite(direction.lengthSq()) || direction.lengthSq() < 1e-12) { + return null; + } + + return direction.normalize(); +} + +function orientSpacecraftTowardEarth(object3D, heliocentricPosition, spacecraftKey) { + const config = getSpacecraftConfig(spacecraftKey); + const orientationGroup = object3D?.userData?.orientationGroup || object3D; + if (!config || !orientationGroup) { + return; + } + + if (config.orientationMode !== 'look_at_earth') { + orientationGroup.quaternion.identity(); + return; + } + + const referenceDirection = getSpacecraftReferenceEarthDirection(spacecraftKey); + const currentDirection = getSpacecraftCurrentEarthDirection(heliocentricPosition); + + if (!referenceDirection || !currentDirection) { + orientationGroup.quaternion.identity(); + return; + } + + orientationGroup.quaternion.setFromUnitVectors(referenceDirection, currentDirection); +} + function getPlanetRotationState(key, date) { try { const axisInfo = AstronomyEngine.RotationAxis(getPlanetBodyKey(key), date); @@ -1549,15 +1659,70 @@ function buildLabels() { labelsLayer.appendChild(pioneer10LabelElement); } -function ensureVoyager1Objects() { - if (voyager1Objects || !Array.isArray(VOYAGER1_TRACK) || VOYAGER1_TRACK.length === 0) { +function getSpacecraftLabelElement(spacecraftKey) { + switch (String(spacecraftKey)) { + case 'voyager1': + return voyager1LabelElement; + case 'voyager2': + return voyager2LabelElement; + case 'new_horizons': + return newHorizonsLabelElement; + case 'pioneer10': + return pioneer10LabelElement; + default: + return null; + } +} + +function getSpacecraftObjects(spacecraftKey) { + switch (String(spacecraftKey)) { + case 'voyager1': + return voyager1Objects; + case 'voyager2': + return voyager2Objects; + case 'new_horizons': + return newHorizonsObjects; + case 'pioneer10': + return pioneer10Objects; + default: + return null; + } +} + +function setSpacecraftObjects(spacecraftKey, objects) { + switch (String(spacecraftKey)) { + case 'voyager1': + voyager1Objects = objects; + break; + case 'voyager2': + voyager2Objects = objects; + break; + case 'new_horizons': + newHorizonsObjects = objects; + break; + case 'pioneer10': + pioneer10Objects = objects; + break; + default: + break; + } +} + +function ensureSpacecraftObjects(spacecraftKey) { + if (getSpacecraftObjects(spacecraftKey)) { + return; + } + + const config = getSpacecraftConfig(spacecraftKey); + const track = getSpacecraftTrack(spacecraftKey); + if (!config || !Array.isArray(track) || track.length === 0) { return; } const orbitLine = new THREE.Line( new THREE.BufferGeometry(), new THREE.LineBasicMaterial({ - color: new THREE.Color('#8fe3ff'), + color: new THREE.Color(config.orbitColor), transparent: true, opacity: 0.8, }) @@ -1565,386 +1730,149 @@ function ensureVoyager1Objects() { orbitLine.visible = showOrbits; scene.add(orbitLine); - const mesh = createVoyagerDisplayObject('#b7f0ff', '#4db5d4'); + const mesh = createSpacecraftDisplayObject(spacecraftKey); + if (!mesh) { + return; + } scene.add(mesh); - voyager1Objects = { orbitLine, mesh, labelElement: voyager1LabelElement }; + setSpacecraftObjects(spacecraftKey, { + orbitLine, + mesh, + labelElement: getSpacecraftLabelElement(spacecraftKey), + }); +} + +function rebuildSpacecraftTrack(spacecraftKey) { + ensureSpacecraftObjects(spacecraftKey); + const spacecraftObjects = getSpacecraftObjects(spacecraftKey); + const track = getSpacecraftTrack(spacecraftKey); + if (!spacecraftObjects || !Array.isArray(track) || track.length === 0) { + return; + } + + const positions = []; + track.forEach((entry) => { + const display = toDisplayPosition({ + x: Number(entry.xAu), + y: Number(entry.yAu), + z: Number(entry.zAu), + r: Math.max(Number(entry.distanceAu), 1e-9), + }); + positions.push(display.x, display.y, display.z); + }); + + spacecraftObjects.orbitLine.geometry.dispose(); + spacecraftObjects.orbitLine.geometry = new THREE.BufferGeometry(); + spacecraftObjects.orbitLine.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); + spacecraftObjects.orbitLine.visible = showOrbits; +} + +function updateSpacecraftVisual(spacecraftKey, isFocused, updateInfoPanel) { + ensureSpacecraftObjects(spacecraftKey); + const spacecraftObjects = getSpacecraftObjects(spacecraftKey); + if (!spacecraftObjects) { + return; + } + + if (!isSpacecraftSelected(spacecraftKey)) { + spacecraftObjects.mesh.visible = false; + spacecraftObjects.orbitLine.visible = false; + if (spacecraftObjects.labelElement) { + spacecraftObjects.labelElement.hidden = true; + } + return; + } + + const position = getSpacecraftPositionAtDate(spacecraftKey, simDate); + if (!position) { + spacecraftObjects.mesh.visible = false; + spacecraftObjects.orbitLine.visible = false; + if (spacecraftObjects.labelElement) { + spacecraftObjects.labelElement.hidden = true; + } + return; + } + + spacecraftObjects.mesh.visible = true; + spacecraftObjects.mesh.position.copy(toDisplayPosition({ + x: Number(position.xAu), + y: Number(position.yAu), + z: Number(position.zAu), + r: Math.max(Number(position.distanceAu), 1e-9), + })); + orientSpacecraftTowardEarth(spacecraftObjects.mesh, position, spacecraftKey); + spacecraftObjects.orbitLine.visible = showOrbits; + + if (isFocused) { + updateInfoPanel(); + } +} + +function ensureVoyager1Objects() { + ensureSpacecraftObjects('voyager1'); } function rebuildVoyager1Track() { - ensureVoyager1Objects(); - if (!voyager1Objects) { - return; - } - - const positions = []; - VOYAGER1_TRACK.forEach((entry) => { - const display = toDisplayPosition({ - x: Number(entry.xAu), - y: Number(entry.yAu), - z: Number(entry.zAu), - r: Math.max(Number(entry.distanceAu), 1e-9), - }); - positions.push(display.x, display.y, display.z); - }); - - voyager1Objects.orbitLine.geometry.dispose(); - voyager1Objects.orbitLine.geometry = new THREE.BufferGeometry(); - voyager1Objects.orbitLine.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); - voyager1Objects.orbitLine.visible = showOrbits; + rebuildSpacecraftTrack('voyager1'); } function updateVoyager1Visual() { - ensureVoyager1Objects(); - if (!voyager1Objects) { - return; - } - - if (!isSpacecraftSelected('voyager1')) { - voyager1Objects.mesh.visible = false; - voyager1Objects.orbitLine.visible = false; - if (voyager1Objects.labelElement) { - voyager1Objects.labelElement.hidden = true; - } - return; - } - - const position = getVoyager1Position(simDate); - if (!position) { - voyager1Objects.mesh.visible = false; - voyager1Objects.orbitLine.visible = false; - if (voyager1Objects.labelElement) { - voyager1Objects.labelElement.hidden = true; - } - return; - } - - voyager1Objects.mesh.visible = true; - voyager1Objects.mesh.position.copy(toDisplayPosition({ - x: Number(position.xAu), - y: Number(position.yAu), - z: Number(position.zAu), - r: Math.max(Number(position.distanceAu), 1e-9), - })); - voyager1Objects.orbitLine.visible = showOrbits; - - if (focusedVoyager1) { - updateVoyager1InfoPanel(); - } + updateSpacecraftVisual('voyager1', focusedVoyager1, updateVoyager1InfoPanel); } function ensureVoyager2Objects() { - if (voyager2Objects || !Array.isArray(VOYAGER2_TRACK) || VOYAGER2_TRACK.length === 0) { - return; - } - - const orbitLine = new THREE.Line( - new THREE.BufferGeometry(), - new THREE.LineBasicMaterial({ - color: new THREE.Color('#ffd36f'), - transparent: true, - opacity: 0.8, - }) - ); - orbitLine.visible = showOrbits; - scene.add(orbitLine); - - const mesh = createVoyagerDisplayObject('#ffe4a6', '#d4972e'); - scene.add(mesh); - - voyager2Objects = { orbitLine, mesh, labelElement: voyager2LabelElement }; + ensureSpacecraftObjects('voyager2'); } function rebuildVoyager2Track() { - ensureVoyager2Objects(); - if (!voyager2Objects) { - return; - } - - const positions = []; - VOYAGER2_TRACK.forEach((entry) => { - const display = toDisplayPosition({ - x: Number(entry.xAu), - y: Number(entry.yAu), - z: Number(entry.zAu), - r: Math.max(Number(entry.distanceAu), 1e-9), - }); - positions.push(display.x, display.y, display.z); - }); - - voyager2Objects.orbitLine.geometry.dispose(); - voyager2Objects.orbitLine.geometry = new THREE.BufferGeometry(); - voyager2Objects.orbitLine.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); - voyager2Objects.orbitLine.visible = showOrbits; + rebuildSpacecraftTrack('voyager2'); } function updateVoyager2Visual() { - ensureVoyager2Objects(); - if (!voyager2Objects) { - return; - } - - if (!isSpacecraftSelected('voyager2')) { - voyager2Objects.mesh.visible = false; - voyager2Objects.orbitLine.visible = false; - if (voyager2Objects.labelElement) { - voyager2Objects.labelElement.hidden = true; - } - return; - } - - const position = getVoyager2Position(simDate); - if (!position) { - voyager2Objects.mesh.visible = false; - voyager2Objects.orbitLine.visible = false; - if (voyager2Objects.labelElement) { - voyager2Objects.labelElement.hidden = true; - } - return; - } - - voyager2Objects.mesh.visible = true; - voyager2Objects.mesh.position.copy(toDisplayPosition({ - x: Number(position.xAu), - y: Number(position.yAu), - z: Number(position.zAu), - r: Math.max(Number(position.distanceAu), 1e-9), - })); - voyager2Objects.orbitLine.visible = showOrbits; - - if (focusedVoyager2) { - updateVoyager2InfoPanel(); - } + updateSpacecraftVisual('voyager2', focusedVoyager2, updateVoyager2InfoPanel); } function ensureNewHorizonsObjects() { - if (newHorizonsObjects || !Array.isArray(NEW_HORIZONS_TRACK) || NEW_HORIZONS_TRACK.length === 0) { - return; - } - - const orbitLine = new THREE.Line( - new THREE.BufferGeometry(), - new THREE.LineBasicMaterial({ - color: new THREE.Color('#ff9fd6'), - transparent: true, - opacity: 0.8, - }) - ); - orbitLine.visible = showOrbits; - scene.add(orbitLine); - - const mesh = new THREE.Mesh( - new THREE.SphereGeometry(0.28, 20, 20), - new THREE.MeshStandardMaterial({ - color: new THREE.Color('#ffd4ec'), - emissive: new THREE.Color('#cc4da0'), - emissiveIntensity: 0.45, - roughness: 0.45, - metalness: 0.7, - }) - ); - mesh.userData.baseScale = 1; - mesh.userData.minScaleMultiplier = 1.8; - scene.add(mesh); - - newHorizonsObjects = { orbitLine, mesh, labelElement: newHorizonsLabelElement }; + ensureSpacecraftObjects('new_horizons'); } function rebuildNewHorizonsTrack() { - ensureNewHorizonsObjects(); - if (!newHorizonsObjects) { - return; - } - - const positions = []; - NEW_HORIZONS_TRACK.forEach((entry) => { - const display = toDisplayPosition({ - x: Number(entry.xAu), - y: Number(entry.yAu), - z: Number(entry.zAu), - r: Math.max(Number(entry.distanceAu), 1e-9), - }); - positions.push(display.x, display.y, display.z); - }); - - newHorizonsObjects.orbitLine.geometry.dispose(); - newHorizonsObjects.orbitLine.geometry = new THREE.BufferGeometry(); - newHorizonsObjects.orbitLine.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); - newHorizonsObjects.orbitLine.visible = showOrbits; + rebuildSpacecraftTrack('new_horizons'); } function updateNewHorizonsVisual() { - ensureNewHorizonsObjects(); - if (!newHorizonsObjects) { - return; - } - - if (!isSpacecraftSelected('new_horizons')) { - newHorizonsObjects.mesh.visible = false; - newHorizonsObjects.orbitLine.visible = false; - if (newHorizonsObjects.labelElement) { - newHorizonsObjects.labelElement.hidden = true; - } - return; - } - - const position = getNewHorizonsPosition(simDate); - if (!position) { - newHorizonsObjects.mesh.visible = false; - newHorizonsObjects.orbitLine.visible = false; - if (newHorizonsObjects.labelElement) { - newHorizonsObjects.labelElement.hidden = true; - } - return; - } - - newHorizonsObjects.mesh.visible = true; - newHorizonsObjects.mesh.position.copy(toDisplayPosition({ - x: Number(position.xAu), - y: Number(position.yAu), - z: Number(position.zAu), - r: Math.max(Number(position.distanceAu), 1e-9), - })); - newHorizonsObjects.orbitLine.visible = showOrbits; - - if (focusedNewHorizons) { - updateNewHorizonsInfoPanel(); - } + updateSpacecraftVisual('new_horizons', focusedNewHorizons, updateNewHorizonsInfoPanel); } function ensurePioneer10Objects() { - if (pioneer10Objects || !Array.isArray(PIONEER10_TRACK) || PIONEER10_TRACK.length === 0) { - return; - } - - const orbitLine = new THREE.Line( - new THREE.BufferGeometry(), - new THREE.LineBasicMaterial({ - color: new THREE.Color('#f3a86f'), - transparent: true, - opacity: 0.8, - }) - ); - orbitLine.visible = showOrbits; - scene.add(orbitLine); - - const mesh = new THREE.Mesh( - new THREE.SphereGeometry(0.28, 20, 20), - new THREE.MeshStandardMaterial({ - color: new THREE.Color('#f7c49c'), - emissive: new THREE.Color('#bc6a2e'), - emissiveIntensity: 0.45, - roughness: 0.45, - metalness: 0.7, - }) - ); - mesh.userData.baseScale = 1; - mesh.userData.minScaleMultiplier = 1.8; - scene.add(mesh); - - pioneer10Objects = { orbitLine, mesh, labelElement: pioneer10LabelElement }; + ensureSpacecraftObjects('pioneer10'); } function rebuildPioneer10Track() { - ensurePioneer10Objects(); - if (!pioneer10Objects) { - return; - } - - const positions = []; - PIONEER10_TRACK.forEach((entry) => { - const display = toDisplayPosition({ - x: Number(entry.xAu), - y: Number(entry.yAu), - z: Number(entry.zAu), - r: Math.max(Number(entry.distanceAu), 1e-9), - }); - positions.push(display.x, display.y, display.z); - }); - - pioneer10Objects.orbitLine.geometry.dispose(); - pioneer10Objects.orbitLine.geometry = new THREE.BufferGeometry(); - pioneer10Objects.orbitLine.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); - pioneer10Objects.orbitLine.visible = showOrbits; + rebuildSpacecraftTrack('pioneer10'); } function updatePioneer10Visual() { - ensurePioneer10Objects(); - if (!pioneer10Objects) { - return; - } - - if (!isSpacecraftSelected('pioneer10')) { - pioneer10Objects.mesh.visible = false; - pioneer10Objects.orbitLine.visible = false; - if (pioneer10Objects.labelElement) { - pioneer10Objects.labelElement.hidden = true; - } - return; - } - - const position = getPioneer10Position(simDate); - if (!position) { - pioneer10Objects.mesh.visible = false; - pioneer10Objects.orbitLine.visible = false; - if (pioneer10Objects.labelElement) { - pioneer10Objects.labelElement.hidden = true; - } - return; - } - - pioneer10Objects.mesh.visible = true; - pioneer10Objects.mesh.position.copy(toDisplayPosition({ - x: Number(position.xAu), - y: Number(position.yAu), - z: Number(position.zAu), - r: Math.max(Number(position.distanceAu), 1e-9), - })); - pioneer10Objects.orbitLine.visible = showOrbits; - - if (focusedPioneer10) { - updatePioneer10InfoPanel(); - } + updateSpacecraftVisual('pioneer10', focusedPioneer10, updatePioneer10InfoPanel); } function syncSpacecraftObjects() { const selectedSpacecraftSet = getSelectedSpacecraftSet(); - if (voyager1Objects) { - const isSelected = selectedSpacecraftSet.has('voyager1'); - voyager1Objects.mesh.visible = isSelected && voyager1Objects.mesh.visible; - voyager1Objects.orbitLine.visible = isSelected && showOrbits && voyager1Objects.orbitLine.visible; - if (!isSelected && voyager1Objects.labelElement) { - voyager1Objects.labelElement.hidden = true; + ['voyager1', 'voyager2', 'new_horizons', 'pioneer10'].forEach((spacecraftKey) => { + const spacecraftObjects = getSpacecraftObjects(spacecraftKey); + if (!spacecraftObjects) { + return; } - } - if (voyager2Objects) { - const isSelected = selectedSpacecraftSet.has('voyager2'); - voyager2Objects.mesh.visible = isSelected && voyager2Objects.mesh.visible; - voyager2Objects.orbitLine.visible = isSelected && showOrbits && voyager2Objects.orbitLine.visible; - if (!isSelected && voyager2Objects.labelElement) { - voyager2Objects.labelElement.hidden = true; + const isSelected = selectedSpacecraftSet.has(spacecraftKey); + spacecraftObjects.mesh.visible = isSelected && spacecraftObjects.mesh.visible; + spacecraftObjects.orbitLine.visible = isSelected && showOrbits && spacecraftObjects.orbitLine.visible; + if (!isSelected && spacecraftObjects.labelElement) { + spacecraftObjects.labelElement.hidden = true; } - } - - if (newHorizonsObjects) { - const isSelected = selectedSpacecraftSet.has('new_horizons'); - newHorizonsObjects.mesh.visible = isSelected && newHorizonsObjects.mesh.visible; - newHorizonsObjects.orbitLine.visible = isSelected && showOrbits && newHorizonsObjects.orbitLine.visible; - if (!isSelected && newHorizonsObjects.labelElement) { - newHorizonsObjects.labelElement.hidden = true; - } - } - - if (pioneer10Objects) { - const isSelected = selectedSpacecraftSet.has('pioneer10'); - pioneer10Objects.mesh.visible = isSelected && pioneer10Objects.mesh.visible; - pioneer10Objects.orbitLine.visible = isSelected && showOrbits && pioneer10Objects.orbitLine.visible; - if (!isSelected && pioneer10Objects.labelElement) { - pioneer10Objects.labelElement.hidden = true; - } - } + }); if (focusedVoyager1 && !selectedSpacecraftSet.has('voyager1')) { clearFocus(); @@ -3527,7 +3455,9 @@ async function initSolarSystem() { createEclipticPlane(); buildLabels(); - await loadVoyagerModelTemplate(); + await Promise.all( + Object.keys(SPACECRAFT_CONFIGS).map((spacecraftKey) => loadSpacecraftModelTemplate(spacecraftKey)) + ); PLANETS.forEach(createPlanetMesh); ensureMoonObjects(); ensureMinorPlanetObjects();