diff --git a/public/css/style.css b/public/css/style.css
index 864fc8f..a8a0e8f 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -3831,9 +3831,9 @@ body.solarsystem-page main > .container {
}
.solarsystem-input-row input[type="date"],
-.solarsystem-input-row select {
+.solarsystem-input-row select,
+.solarsystem-multiselect {
min-height: 2.6rem;
- padding: 0.55rem 0.8rem;
border-radius: 6px;
border: 1px solid rgba(200, 168, 75, 0.14);
background: rgba(10, 16, 36, 0.8);
@@ -3843,6 +3843,11 @@ body.solarsystem-page main > .container {
flex: 1 1 170px;
}
+.solarsystem-input-row input[type="date"],
+.solarsystem-input-row select {
+ padding: 0.55rem 0.8rem;
+}
+
.solarsystem-input-row select {
appearance: none;
-webkit-appearance: none;
@@ -3882,6 +3887,102 @@ body.solarsystem-page main > .container {
box-shadow: none;
}
+.solarsystem-multiselect {
+ position: relative;
+ box-shadow: inset 0 0 0 1px rgba(255,255,255,0.02), 0 10px 24px rgba(0, 0, 0, 0.22);
+ overflow: hidden;
+}
+
+.solarsystem-multiselect:hover {
+ border-color: rgba(201, 168, 76, 0.28);
+ background-color: rgba(14, 22, 44, 0.92);
+ box-shadow: inset 0 0 0 1px rgba(201, 168, 76, 0.05), 0 14px 28px rgba(0, 0, 0, 0.26);
+}
+
+.solarsystem-multiselect[open] {
+ border-color: rgba(201, 168, 76, 0.4);
+ box-shadow: 0 0 0 3px rgba(201, 168, 76, 0.12), 0 14px 28px rgba(0, 0, 0, 0.28);
+}
+
+.solarsystem-multiselect summary {
+ list-style: none;
+ cursor: pointer;
+ min-height: 2.6rem;
+ padding: 0.55rem 2.9rem 0.55rem 0.8rem;
+ display: flex;
+ align-items: center;
+ position: relative;
+}
+
+.solarsystem-multiselect summary::-webkit-details-marker {
+ display: none;
+}
+
+.solarsystem-multiselect summary::before,
+.solarsystem-multiselect summary::after {
+ content: "";
+ position: absolute;
+ right: 1rem;
+ top: 50%;
+ width: 0.5rem;
+ height: 0.5rem;
+ border-right: 2px solid rgba(240, 217, 144, 0.92);
+ border-bottom: 2px solid rgba(240, 217, 144, 0.92);
+ transform-origin: center;
+}
+
+.solarsystem-multiselect summary::before {
+ transform: translateY(-60%) rotate(45deg);
+}
+
+.solarsystem-multiselect[open] summary::before {
+ transform: translateY(-20%) rotate(225deg);
+}
+
+.solarsystem-multiselect summary::after {
+ display: none;
+}
+
+.solarsystem-multiselect-panel {
+ display: grid;
+ gap: 0.3rem;
+ max-height: 15rem;
+ overflow-y: auto;
+ padding: 0 0.45rem 0.45rem;
+}
+
+.solarsystem-multiselect-option {
+ display: flex;
+ align-items: center;
+ gap: 0.55rem;
+ padding: 0.45rem 0.5rem;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+}
+
+.solarsystem-multiselect-option:hover {
+ background: rgba(255, 255, 255, 0.04);
+}
+
+.solarsystem-multiselect-option input {
+ margin: 0;
+ accent-color: #f0d990;
+}
+
+.solarsystem-multiselect-option span {
+ min-width: 0;
+}
+
+.solarsystem-multiselect.is-disabled {
+ opacity: 0.7;
+ box-shadow: none;
+}
+
+.solarsystem-multiselect.is-disabled summary {
+ cursor: default;
+}
+
.solarsystem-current-date,
.solarsystem-speed-value {
color: var(--gold-pale);
diff --git a/public/solarsystem.php b/public/solarsystem.php
index 7a4f4af..8ed2215 100644
--- a/public/solarsystem.php
+++ b/public/solarsystem.php
@@ -194,15 +194,28 @@ $bodyClass = 'solarsystem-page';
@@ -212,7 +225,7 @@ $bodyClass = 'solarsystem-page';
Maus ziehen: Kamera drehen
Mausrad: Zoom
Klick auf Planet: Fokus setzen
- Dropdown: gewaehlten Kleinplaneten-Orbit zeigen
+ Dropdown: mehrere Kleinplaneten per Haken anzeigen
Erneuter Klick ins Leere: Fokus loesen
@@ -314,7 +327,9 @@ const btnEcliptic = document.getElementById('solarBtnEcliptic');
const speedSlider = document.getElementById('solarSpeedSlider');
const speedLabel = document.getElementById('solarSpeedLabel');
const infoPanel = document.getElementById('solarInfoPanel');
-const minorPlanetSelect = document.getElementById('solarMinorplanetSelect');
+const minorPlanetSelectWrap = document.getElementById('solarMinorplanetSelectWrap');
+const minorPlanetSelectSummary = document.getElementById('solarMinorplanetSelectSummary');
+const minorPlanetCheckboxes = Array.from(document.querySelectorAll('.solar-minorplanet-checkbox'));
if (!AstronomyEngine) {
overlay.textContent = 'Astronomy Engine konnte nicht geladen werden.';
@@ -336,9 +351,7 @@ let rotationEqjToEcl;
let moonMesh = null;
let moonOrbitLine = null;
let moonLabelElement = null;
-let minorPlanetOrbitLine = null;
-let minorPlanetMesh = null;
-let minorPlanetLabelElement = null;
+let minorPlanetObjects = null;
const planetDynamicsCache = new Map();
let showOrbits = true;
@@ -347,7 +360,7 @@ let showEcliptic = false;
let playing = false;
let speedDaysPerSecond = 1;
let focusedPlanetKey = null;
-let selectedMinorPlanetId = null;
+let selectedMinorPlanetIds = [];
let focusedMinorPlanetId = null;
let simDate = new Date();
let lastAnimationTime = performance.now();
@@ -453,6 +466,48 @@ function getMinorPlanetMeta(id) {
return USER_MINOR_PLANETS.find((minorPlanet) => minorPlanet.id === Number(id)) || null;
}
+function getSelectedMinorPlanetSet() {
+ return new Set(selectedMinorPlanetIds);
+}
+
+const MINOR_PLANET_COLOR_PALETTE = [
+ { line: '#ff6b6b', body: '#ff5a5a', emissive: '#d63b3b' },
+ { line: '#ff5252', body: '#ff4747', emissive: '#c62828' },
+ { line: '#ff7a5c', body: '#ff6b4a', emissive: '#d9482c' },
+ { line: '#ff4d6d', body: '#ff3f5f', emissive: '#c81f45' },
+ { line: '#ff8a65', body: '#ff7043', emissive: '#d4512c' },
+ { line: '#f0625d', body: '#eb554f', emissive: '#bf3c36' },
+];
+
+function getMinorPlanetColor(id) {
+ const safeId = Math.abs(Number(id) || 0);
+ return MINOR_PLANET_COLOR_PALETTE[safeId % MINOR_PLANET_COLOR_PALETTE.length];
+}
+
+function updateMinorPlanetSummary() {
+ if (!minorPlanetSelectSummary) {
+ return;
+ }
+
+ if (minorPlanetCheckboxes.length === 0) {
+ minorPlanetSelectSummary.textContent = 'Keine Kleinplaneten verfuegbar';
+ return;
+ }
+
+ if (selectedMinorPlanetIds.length === 0) {
+ minorPlanetSelectSummary.textContent = 'Kleinplaneten auswaehlen';
+ return;
+ }
+
+ const selectedNames = selectedMinorPlanetIds
+ .map((id) => formatMinorPlanetName(getMinorPlanetMeta(id)))
+ .filter((name) => name && name !== '--');
+
+ minorPlanetSelectSummary.textContent = selectedNames.length <= 2
+ ? selectedNames.join(', ')
+ : `${selectedNames.length} Kleinplaneten ausgewaehlt`;
+}
+
function formatMinorPlanetName(minorPlanet) {
if (!minorPlanet) {
return '--';
@@ -658,9 +713,10 @@ function updateMinorPlanetInfoPanel(id) {
);
const apparentMagnitude = estimateMinorPlanetMagnitude(minorPlanet, position, earthPosition, earthDistanceAu);
const periodDays = minorPlanet.meanMotionDegPerDay > 0 ? 360 / minorPlanet.meanMotionDegPerDay : null;
+ const color = getMinorPlanetColor(id);
infoPanel.hidden = false;
document.getElementById('solarInfoName').textContent = formatMinorPlanetName(minorPlanet);
- document.getElementById('solarInfoName').style.color = '#ff2a2a';
+ document.getElementById('solarInfoName').style.color = color.line;
document.getElementById('solarInfoDistanceAu').textContent = `${position.r.toFixed(4)} AU`;
document.getElementById('solarInfoDistanceKm').textContent = `${(position.r * AU_IN_KM).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, '.')} km`;
document.getElementById('solarInfoEarthDistanceAu').textContent = `${earthDistanceAu.toFixed(4)} AU`;
@@ -773,39 +829,86 @@ function updateMoonVisual() {
}
function ensureMinorPlanetObjects() {
- if (minorPlanetOrbitLine && minorPlanetMesh && minorPlanetLabelElement) {
- return;
+ if (!minorPlanetObjects) {
+ minorPlanetObjects = new Map();
+ }
+}
+
+function createMinorPlanetObjects(id) {
+ ensureMinorPlanetObjects();
+
+ if (minorPlanetObjects.has(id)) {
+ return minorPlanetObjects.get(id);
}
+ const color = getMinorPlanetColor(id);
const lineMaterial = new THREE.LineBasicMaterial({
- color: 0xff3333,
+ color: new THREE.Color(color.line),
transparent: true,
opacity: 0.85
});
- minorPlanetOrbitLine = new THREE.Line(new THREE.BufferGeometry(), lineMaterial);
- minorPlanetOrbitLine.visible = false;
- scene.add(minorPlanetOrbitLine);
+ const orbitLine = new THREE.Line(new THREE.BufferGeometry(), lineMaterial);
+ orbitLine.visible = false;
+ scene.add(orbitLine);
- minorPlanetMesh = new THREE.Mesh(
+ const mesh = new THREE.Mesh(
new THREE.SphereGeometry(0.58, 28, 28),
new THREE.MeshStandardMaterial({
- color: 0xff3030,
- emissive: new THREE.Color(0xcc0000),
+ color: new THREE.Color(color.body),
+ emissive: new THREE.Color(color.emissive),
emissiveIntensity: 0.06,
roughness: 0.65,
metalness: 0.08,
})
);
- minorPlanetMesh.visible = false;
- minorPlanetMesh.userData.baseScale = 1;
- minorPlanetMesh.userData.minScaleMultiplier = 1;
- scene.add(minorPlanetMesh);
+ mesh.visible = false;
+ mesh.userData.baseScale = 1;
+ mesh.userData.minScaleMultiplier = 1;
+ mesh.userData.minorPlanetId = id;
+ scene.add(mesh);
- minorPlanetLabelElement = document.createElement('div');
- minorPlanetLabelElement.className = 'solarsystem-planet-label';
- minorPlanetLabelElement.style.color = '#ff2a2a';
- minorPlanetLabelElement.hidden = true;
- labelsLayer.appendChild(minorPlanetLabelElement);
+ const labelElement = document.createElement('div');
+ labelElement.className = 'solarsystem-planet-label';
+ labelElement.style.color = color.line;
+ labelElement.hidden = true;
+ labelsLayer.appendChild(labelElement);
+
+ const objects = { orbitLine, mesh, labelElement };
+ minorPlanetObjects.set(id, objects);
+ return objects;
+}
+
+function removeMinorPlanetObjects(id) {
+ if (!minorPlanetObjects || !minorPlanetObjects.has(id)) {
+ return;
+ }
+
+ const { orbitLine, mesh, labelElement } = minorPlanetObjects.get(id);
+ orbitLine.geometry.dispose();
+ orbitLine.material.dispose();
+ scene.remove(orbitLine);
+
+ mesh.geometry.dispose();
+ mesh.material.dispose();
+ scene.remove(mesh);
+
+ labelElement.remove();
+ minorPlanetObjects.delete(id);
+}
+
+function syncMinorPlanetObjects() {
+ ensureMinorPlanetObjects();
+
+ const selectedSet = getSelectedMinorPlanetSet();
+ selectedMinorPlanetIds.forEach((id) => {
+ createMinorPlanetObjects(id);
+ });
+
+ Array.from(minorPlanetObjects.keys()).forEach((id) => {
+ if (!selectedSet.has(id)) {
+ removeMinorPlanetObjects(id);
+ }
+ });
}
function normalizeRadians(value) {
@@ -890,13 +993,9 @@ function getMinorPlanetHelioPosition(minorPlanet, date) {
}
function rebuildMinorPlanetOrbit(id) {
- ensureMinorPlanetObjects();
-
const minorPlanet = getMinorPlanetMeta(id);
- if (!minorPlanet) {
- minorPlanetOrbitLine.visible = false;
- minorPlanetMesh.visible = false;
- minorPlanetLabelElement.hidden = true;
+ const objects = createMinorPlanetObjects(id);
+ if (!minorPlanet || !objects) {
return;
}
@@ -914,42 +1013,57 @@ function rebuildMinorPlanetOrbit(id) {
positions.push(display.x, display.y, display.z);
}
- minorPlanetOrbitLine.geometry.dispose();
- minorPlanetOrbitLine.geometry = new THREE.BufferGeometry();
- minorPlanetOrbitLine.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
- minorPlanetOrbitLine.visible = showOrbits;
+ objects.orbitLine.geometry.dispose();
+ objects.orbitLine.geometry = new THREE.BufferGeometry();
+ objects.orbitLine.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
+ objects.orbitLine.visible = showOrbits;
}
function updateMinorPlanetVisual() {
- ensureMinorPlanetObjects();
+ syncMinorPlanetObjects();
- const minorPlanet = getMinorPlanetMeta(selectedMinorPlanetId);
- if (!minorPlanet) {
- minorPlanetOrbitLine.visible = false;
- minorPlanetMesh.visible = false;
- minorPlanetLabelElement.hidden = true;
+ if (!minorPlanetObjects || minorPlanetObjects.size === 0) {
if (!focusedPlanetKey) {
infoPanel.hidden = true;
}
return;
}
- const heliocentric = getMinorPlanetHelioPosition(minorPlanet, simDate);
- if (!heliocentric) {
- minorPlanetOrbitLine.visible = false;
- minorPlanetMesh.visible = false;
- minorPlanetLabelElement.hidden = true;
- return;
- }
+ const selectedSet = getSelectedMinorPlanetSet();
+ minorPlanetObjects.forEach((objects, id) => {
+ const minorPlanet = getMinorPlanetMeta(id);
+ const isSelected = selectedSet.has(id);
+ if (!minorPlanet || !isSelected) {
+ objects.orbitLine.visible = false;
+ objects.mesh.visible = false;
+ objects.labelElement.hidden = true;
+ return;
+ }
- const display = toDisplayPosition(heliocentric);
- minorPlanetMesh.visible = true;
- minorPlanetMesh.position.copy(display);
- minorPlanetMesh.rotation.y = (simDate.getTime() / 1000) * 0.0002;
- minorPlanetOrbitLine.visible = showOrbits;
+ const heliocentric = getMinorPlanetHelioPosition(minorPlanet, simDate);
+ if (!heliocentric) {
+ objects.orbitLine.visible = false;
+ objects.mesh.visible = false;
+ objects.labelElement.hidden = true;
+ return;
+ }
- if (focusedMinorPlanetId === selectedMinorPlanetId) {
- updateMinorPlanetInfoPanel(selectedMinorPlanetId);
+ const display = toDisplayPosition(heliocentric);
+ objects.mesh.visible = true;
+ objects.mesh.position.copy(display);
+ objects.mesh.rotation.y = (simDate.getTime() / 1000) * 0.0002;
+ objects.orbitLine.visible = showOrbits;
+
+ if (focusedMinorPlanetId === id) {
+ updateMinorPlanetInfoPanel(id);
+ }
+ });
+
+ if (focusedMinorPlanetId !== null && !selectedSet.has(focusedMinorPlanetId)) {
+ focusedMinorPlanetId = null;
+ if (!focusedPlanetKey) {
+ infoPanel.hidden = true;
+ }
}
}
@@ -1150,8 +1264,12 @@ function updateBodyDisplayScales() {
applyMinimumDisplayScale(moonMesh, 220, 10);
}
- if (minorPlanetMesh && minorPlanetMesh.visible) {
- applyMinimumDisplayScale(minorPlanetMesh, 220, 10);
+ if (minorPlanetObjects) {
+ minorPlanetObjects.forEach(({ mesh }) => {
+ if (mesh.visible) {
+ applyMinimumDisplayScale(mesh, 220, 10);
+ }
+ });
}
}
@@ -1193,26 +1311,28 @@ function updateLabels() {
}
}
- if (!minorPlanetLabelElement || !minorPlanetMesh) {
+ if (!minorPlanetObjects) {
return;
}
- const minorPlanet = getMinorPlanetMeta(selectedMinorPlanetId);
- minorPlanetLabelElement.hidden = !showLabels || !minorPlanet || !minorPlanetMesh.visible;
- if (minorPlanetLabelElement.hidden) {
- return;
- }
+ minorPlanetObjects.forEach(({ mesh, labelElement }, id) => {
+ const minorPlanet = getMinorPlanetMeta(id);
+ labelElement.hidden = !showLabels || !minorPlanet || !mesh.visible;
+ if (labelElement.hidden) {
+ return;
+ }
- const projected = minorPlanetMesh.position.clone().project(camera);
- if (projected.z > 1) {
- minorPlanetLabelElement.style.opacity = '0';
- return;
- }
+ const projected = mesh.position.clone().project(camera);
+ if (projected.z > 1) {
+ labelElement.style.opacity = '0';
+ return;
+ }
- minorPlanetLabelElement.textContent = formatMinorPlanetName(minorPlanet);
- minorPlanetLabelElement.style.opacity = '1';
- minorPlanetLabelElement.style.left = `${(projected.x * 0.5 + 0.5) * stageWrap.clientWidth}px`;
- minorPlanetLabelElement.style.top = `${(-projected.y * 0.5 + 0.5) * stageWrap.clientHeight}px`;
+ labelElement.textContent = formatMinorPlanetName(minorPlanet);
+ labelElement.style.opacity = '1';
+ labelElement.style.left = `${(projected.x * 0.5 + 0.5) * stageWrap.clientWidth}px`;
+ labelElement.style.top = `${(-projected.y * 0.5 + 0.5) * stageWrap.clientHeight}px`;
+ });
}
function pickPlanet(clientX, clientY) {
@@ -1230,13 +1350,20 @@ function pickPlanet(clientX, clientY) {
});
const hits = raycaster.intersectObjects(meshes.map((item) => item.mesh), false);
- if (minorPlanetMesh && minorPlanetMesh.visible) {
- const minorPlanetHits = raycaster.intersectObject(minorPlanetMesh, false);
- if (minorPlanetHits.length > 0 && selectedMinorPlanetId) {
- focusedPlanetKey = null;
- focusedMinorPlanetId = selectedMinorPlanetId;
- updateMinorPlanetInfoPanel(selectedMinorPlanetId);
- return;
+ if (minorPlanetObjects) {
+ const visibleMinorPlanetMeshes = Array.from(minorPlanetObjects.entries())
+ .filter(([, objects]) => objects.mesh.visible)
+ .map(([id, objects]) => ({ id, mesh: objects.mesh }));
+
+ const minorPlanetHits = raycaster.intersectObjects(visibleMinorPlanetMeshes.map((item) => item.mesh), false);
+ if (minorPlanetHits.length > 0) {
+ const hitItem = visibleMinorPlanetMeshes.find((item) => item.mesh === minorPlanetHits[0].object);
+ if (hitItem) {
+ focusedPlanetKey = null;
+ focusedMinorPlanetId = hitItem.id;
+ updateMinorPlanetInfoPanel(hitItem.id);
+ return;
+ }
}
}
@@ -1336,8 +1463,10 @@ function attachUiHandlers() {
orbitLines.forEach((line) => {
line.visible = showOrbits;
});
- if (minorPlanetOrbitLine) {
- minorPlanetOrbitLine.visible = showOrbits && !!getMinorPlanetMeta(selectedMinorPlanetId);
+ if (minorPlanetObjects) {
+ minorPlanetObjects.forEach(({ orbitLine }) => {
+ orbitLine.visible = showOrbits;
+ });
}
if (moonOrbitLine) {
moonOrbitLine.visible = showOrbits;
@@ -1372,16 +1501,27 @@ function attachUiHandlers() {
refreshCurrentDateUi();
});
- if (minorPlanetSelect) {
- minorPlanetSelect.addEventListener('change', () => {
- selectedMinorPlanetId = minorPlanetSelect.value ? Number(minorPlanetSelect.value) : null;
- focusedMinorPlanetId = null;
- rebuildMinorPlanetOrbit(selectedMinorPlanetId);
- if (!selectedMinorPlanetId && !focusedPlanetKey) {
+ minorPlanetCheckboxes.forEach((checkbox) => {
+ checkbox.addEventListener('change', () => {
+ selectedMinorPlanetIds = minorPlanetCheckboxes
+ .filter((input) => input.checked)
+ .map((input) => Number(input.value));
+
+ if (focusedMinorPlanetId !== null && !selectedMinorPlanetIds.includes(focusedMinorPlanetId)) {
+ focusedMinorPlanetId = null;
+ }
+
+ syncMinorPlanetObjects();
+ selectedMinorPlanetIds.forEach((id) => {
+ rebuildMinorPlanetOrbit(id);
+ });
+ updateMinorPlanetSummary();
+
+ if (selectedMinorPlanetIds.length === 0 && !focusedPlanetKey && focusedMinorPlanetId === null) {
infoPanel.hidden = true;
}
});
- }
+ });
}
function animate() {
@@ -1419,9 +1559,12 @@ function animate() {
}
}
- if (focusedMinorPlanetId && minorPlanetMesh && minorPlanetMesh.visible) {
- cameraTarget.lerp(minorPlanetMesh.position, 0.06);
- updateMinorPlanetInfoPanel(focusedMinorPlanetId);
+ if (focusedMinorPlanetId && minorPlanetObjects && minorPlanetObjects.has(focusedMinorPlanetId)) {
+ const focusedMinorPlanetObjects = minorPlanetObjects.get(focusedMinorPlanetId);
+ if (focusedMinorPlanetObjects.mesh.visible) {
+ cameraTarget.lerp(focusedMinorPlanetObjects.mesh.position, 0.06);
+ updateMinorPlanetInfoPanel(focusedMinorPlanetId);
+ }
}
camRadius += (camRadiusTarget - camRadius) * 0.18;
@@ -1461,6 +1604,7 @@ async function initSolarSystem() {
pointer = new THREE.Vector2();
cameraTarget = new THREE.Vector3(0, 0, 0);
rotationEqjToEcl = AstronomyEngine.Rotation_EQJ_ECL();
+ updateMinorPlanetSummary();
const ambientLight = new THREE.AmbientLight(0x334466, 0.8);
scene.add(ambientLight);