diff --git a/README.md b/README.md index aef7e02..8dac4a8 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,18 @@ -# The TYCHOSIUM 💫 +# The Tychosium 💫 ![Version](https://img.shields.io/badge/version-1.0.0-blue.svg) ![License](https://img.shields.io/badge/license-GPLv2-blue.svg) ![React](https://img.shields.io/badge/React-18-61DAFB.svg?logo=react&logoColor=black) ![Three.js](https://img.shields.io/badge/Three.js-r162-black.svg?logo=three.js&logoColor=white) -**The TYCHOSIUM** is an interactive 3D astronomical simulation implementing the **TYCHOS model** of our solar system. It offers a unique perspective on celestial mechanics, featuring real-time orbital calculations, a comprehensive star catalog, and immersive visualization. +**The Tychosium** is an interactive 3D astronomical simulation implementing the **TYCHOS model** of our solar system. It offers a unique perspective on celestial mechanics, featuring real-time orbital calculations, a comprehensive star catalog, and immersive visualization. Built with modern web technologies to ensure performance and accuracy, this project aims to visualize the binary solar system concepts proposed by the Tychos model. --- ## 📑 Table of Contents + - [Features](#-features) - [The TYCHOS Model](#-the-tychos-model) - [Technical Stack](#-technical-stack) @@ -27,31 +28,35 @@ Built with modern web technologies to ensure performance and accuracy, this proj ## 🌟 Features ### 🔭 Astronomical Simulation -* **TYCHOS Implementation:** Full realization of the Earth-centered binary system model. -* **Real-time Mechanics:** Accurate planetary motions with the ability to traverse time (past/future). -* **Celestial Bodies:** Includes the Sun, Moon, planets, major asteroids, and Halley's comet. -* **Star Catalog:** Integrated **Bright Star Catalog (BSC)** containing over 9,000 stars with accurate magnitude and color data. + +- **TYCHOS Implementation:** Full realization of the Earth-centered binary system model. +- **Real-time Mechanics:** Accurate planetary motions with the ability to traverse time (past/future). +- **Celestial Bodies:** Includes the Sun, Moon, planets, major asteroids, and Halley's comet. +- **Star Catalog:** Integrated **Bright Star Catalog (BSC)** containing over 9,000 stars with accurate magnitude and color data. ### 🎨 3D Visualization -* **Dual Camera System:** Switch between a global "Orbit Camera" and a surface-level "Planet Camera". -* **Orbital Tracing:** Visualize complex planetary geometric paths (spirographs) over time. -* **Visual Aids:** Ecliptic grids, celestial sphere, zodiacal bands, and polar lines. -* **High-Fidelity Graphics:** Realistic textures and dynamic solar lighting using post-processing effects. + +- **Dual Camera System:** Switch between a global "Orbit Camera" and a surface-level "Planet Camera". +- **Orbital Tracing:** Visualize complex planetary geometric paths (spirographs) over time. +- **Visual Aids:** Ecliptic grids, celestial sphere, zodiacal bands, and polar lines. +- **High-Fidelity Graphics:** Realistic textures and dynamic solar lighting using post-processing effects. ### 🎛️ Advanced Interaction -* **Time Travel:** Jump to specific historical or future dates instantly. -* **Perpetual calendar:** Gregorian dates and support for Astronomical Julian day. -* **Variable Speed:** Control simulation speed from real-time up to millennial steps. -* **Smart Search:** Search implementation to quickly locate stars by name/HR number. + +- **Time Travel:** Jump to specific historical or future dates instantly. +- **Perpetual calendar:** Gregorian dates and support for Astronomical Julian day. +- **Variable Speed:** Control simulation speed from real-time up to millennial steps. +- **Smart Search:** Search implementation to quickly locate stars by name/HR number. --- ## 💫 The TYCHOS Model This simulation is distinct from standard heliocentric visualizers. It implements the TYCHOS model, which proposes: -* **Earth as Reference:** Earth remains relatively stationary at the center of the system. -* **Binary System:** The Sun and Mars are binary companions. -* **PVP Orbit:** The entire solar system rotates together with Earth in the PVP-orbit (Polaris-Vega-Polaris). + +- **Earth as Reference:** Earth remains relatively stationary at the center of the system. +- **Binary System:** The Sun and Mars are binary companions. +- **PVP Orbit:** The entire solar system rotates together with Earth in the PVP-orbit (Polaris-Vega-Polaris). > 📖 **Learn more:** [tychos.space](https://www.tychos.space) @@ -61,33 +66,36 @@ This simulation is distinct from standard heliocentric visualizers. It implement This project leverages the latest ecosystem for 3D web development. -| Category | Technology | Purpose | -|----------|------------|---------| -| **Core** | [React 18](https://reactjs.org/) | UI and Component Architecture | -| **3D Engine** | [Three.js](https://threejs.org/) | WebGL Rendering Engine | -| **Renderer** | [React Three Fiber](https://docs.pmnd.rs/react-three-fiber/) | React renderer for Three.js | -| **Helpers** | [@react-three/drei](https://github.com/pmndrs/drei) | High-level 3D abstractions | -| **Effects** | [Postprocessing](https://github.com/pmndrs/postprocessing) | Bloom, glow, and visual effects | -| **State** | [Zustand](https://github.com/pmndrs/zustand) | Global state management | -| **GUI** | [Leva](https://github.com/pmndrs/leva) | Tweakable control panels | +| Category | Technology | Purpose | +| ------------- | ------------------------------------------------------------ | ------------------------------- | +| **Core** | [React 18](https://reactjs.org/) | UI and Component Architecture | +| **3D Engine** | [Three.js](https://threejs.org/) | WebGL Rendering Engine | +| **Renderer** | [React Three Fiber](https://docs.pmnd.rs/react-three-fiber/) | React renderer for Three.js | +| **Helpers** | [@react-three/drei](https://github.com/pmndrs/drei) | High-level 3D abstractions | +| **Effects** | [Postprocessing](https://github.com/pmndrs/postprocessing) | Bloom, glow, and visual effects | +| **State** | [Zustand](https://github.com/pmndrs/zustand) | Global state management | +| **GUI** | [Leva](https://github.com/pmndrs/leva) | Tweakable control panels | --- ## 🌍 Getting Started ### Prerequisites -* **Node.js**: v16.0.0 or higher -* **Package Manager**: `npm` or `yarn` + +- **Node.js**: v16.0.0 or higher +- **Package Manager**: `npm` or `yarn` ### Installation 1. **Clone the repository** + ```bash git clone [https://github.com/pholmq/TSN.git](https://github.com/pholmq/TSN.git) cd TSN ``` 2. **Install dependencies** + ```bash npm install ``` @@ -99,14 +107,14 @@ This project leverages the latest ecosystem for 3D web development. Open [http://localhost:3000](http://localhost:3000) to view it in the browser. ### Building for Production + To create an optimized build for deployment: + ```bash npm run build ``` - - --- ## 📂 Project Structure @@ -139,15 +147,15 @@ src/ You can customize the simulation logic by editing files in the `src/settings/` directory: -* **`celestial-settings.json`**: Modify orbital speeds, distances, sizes, and starting positions for planets. -* **`star-settings.json`**: Adjust the rendering scale, brightness, and colors of stars. -* **`BSC.json`**: The raw data for the stars. +- **`celestial-settings.json`**: Modify orbital speeds, distances, sizes, and starting positions for planets. +- **`star-settings.json`**: Adjust the rendering scale, brightness, and colors of stars. +- **`BSC.json`**: The raw data for the stars. --- ## 🤝 Contributing -Contributions are welcome! +Contributions are welcome! 1. Fork the Project 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) @@ -165,13 +173,13 @@ This project is licensed under the **GNU General Public License v2.0** - see the ## 🙏 Acknowledgments -* **Simon Shack** for the creation of the TYCHOS model. -* **Astronomers and scientists throughout the ages** for trying to figure out the mysteries of our world. In particular Tycho Brahe. -* **Yale University Observatory** for the Bright Star Catalog. -* **The open-source community** behind **React Three Fiber** and so many other useful things. +- **Simon Shack** for the creation of the TYCHOS model. +- **Astronomers and scientists throughout the ages** for trying to figure out the mysteries of our world. In particular Tycho Brahe. +- **Yale University Observatory** for the Bright Star Catalog. +- **The open-source community** behind **React Three Fiber** and so many other useful things. --- ## 🎁 Donations -You can donate to this work by visiting [tychos.space](http://www.tychos.space) and selecting "Donate". Your gift is much appreciated since Simon Shack has devoted a decade on the Tychos research and since we currently receive no funds of any kind for this work. \ No newline at end of file +You can donate to this work by visiting [tychos.space](http://www.tychos.space) and selecting "Donate". Your gift is much appreciated since Simon Shack has devoted a decade on the Tychos research and since we currently receive no funds of any kind for this work. diff --git a/package-lock.json b/package-lock.json index 33ece5d..61167b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,27 @@ { "name": "the_tychosium", - "version": "1.0.2", + "version": "1.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "the_tychosium", - "version": "1.0.2", + "version": "1.0.3", "license": "GPL-2.0-or-only", "dependencies": { "@react-three/drei": "9.102.6", "@react-three/fiber": "8.15.19", "@react-three/postprocessing": "2.19.0", "@types/three": "0.171.0", + "file-saver": "^2.0.5", "fuse.js": "^7.1.0", "leva": "0.9.34", "lodash-es": "^4.17.21", + "mediabunny": "^1.37.0", + "mobx": "^6.15.0", + "mobx-react": "^9.2.1", "postprocessing": "6.35.2", + "r3f-perf": "^7.2.3", "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "5.4.0", @@ -3737,6 +3742,18 @@ "integrity": "sha512-Rp7ll8BHrKB3wXaRFKhrltwZl1CiXGdibPxuWXvqGnKTnv8fqa/nvftYNuSbf+pbJWKYCXdBtYTITdAUTGGh0Q==", "license": "Apache-2.0" }, + "node_modules/@monogrid/gainmap-js": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz", + "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==", + "license": "MIT", + "dependencies": { + "promise-worker-transferable": "^1.0.4" + }, + "peerDependencies": { + "three": ">= 0.159.0" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -3922,6 +3939,15 @@ "@babel/runtime": "^7.13.10" } }, + "node_modules/@radix-ui/react-icons": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", + "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", + "license": "MIT", + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/@radix-ui/react-presence": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-0.1.1.tgz", @@ -4706,6 +4732,21 @@ "@types/ms": "*" } }, + "node_modules/@types/dom-mediacapture-transform": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.11.tgz", + "integrity": "sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ==", + "license": "MIT", + "dependencies": { + "@types/dom-webcodecs": "*" + } + }, + "node_modules/@types/dom-webcodecs": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz", + "integrity": "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==", + "license": "MIT" + }, "node_modules/@types/draco3d": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", @@ -5467,6 +5508,23 @@ "react": ">= 16.8.0" } }, + "node_modules/@utsubo/events": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@utsubo/events/-/events-0.1.7.tgz", + "integrity": "sha512-WB/GEj/0h27Bz8rJ0+CBtNz5mLT79ne1OjB7PUM4n0qLBqEDwm6yBzZC3j6tasHjlBPJDYZiBVIA1glaMlgZ5g==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.7" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -10712,6 +10770,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "license": "MIT" + }, "node_modules/file-selector": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.5.0.tgz", @@ -11730,6 +11794,12 @@ "he": "bin/he" } }, + "node_modules/hls.js": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz", + "integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==", + "license": "Apache-2.0" + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -12161,6 +12231,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/immer": { "version": "9.0.21", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", @@ -12708,6 +12784,12 @@ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "license": "MIT" }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -14763,6 +14845,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -15254,6 +15345,23 @@ "node": ">= 0.6" } }, + "node_modules/mediabunny": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.37.0.tgz", + "integrity": "sha512-eV7M9IJ29pr/8RNL1sYtIxNbdMfDMN1hMwMaOFfNLhwuKKGSC+eKwiJFpdVjEJ3zrMA4LGerF4Hps0SENFSAlg==", + "license": "MPL-2.0", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@types/dom-mediacapture-transform": "^0.1.11", + "@types/dom-webcodecs": "0.1.13" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/Vanilagy" + } + }, "node_modules/memfs": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", @@ -16008,6 +16116,66 @@ "node": ">=10" } }, + "node_modules/mobx": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.15.0.tgz", + "integrity": "sha512-UczzB+0nnwGotYSgllfARAqWCJ5e/skuV2K/l+Zyck/H6pJIhLXuBnz+6vn2i211o7DtbE78HQtsYEKICHGI+g==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + } + }, + "node_modules/mobx-react": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-9.2.1.tgz", + "integrity": "sha512-WJNNm0FB2n0Z0u+jS1QHmmWyV8l2WiAj8V8I/96kbUEN2YbYCoKW+hbbqKKRUBqElu0llxM7nWKehvRIkhBVJw==", + "license": "MIT", + "dependencies": { + "mobx-react-lite": "^4.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + }, + "peerDependencies": { + "mobx": "^6.9.0", + "react": "^16.8.0 || ^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/mobx-react-lite": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-4.1.1.tgz", + "integrity": "sha512-iUxiMpsvNraCKXU+yPotsOncNNmyeS2B5DKL+TL6Tar/xm+wwNJAubJmtRSeAoYawdZqwv8Z/+5nPRHeQxTiXg==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + }, + "peerDependencies": { + "mobx": "^6.9.0", + "react": "^16.8.0 || ^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -18449,6 +18617,16 @@ "node": ">=10" } }, + "node_modules/promise-worker-transferable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", + "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==", + "license": "Apache-2.0", + "dependencies": { + "is-promise": "^2.1.0", + "lie": "^3.0.2" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -18609,6 +18787,243 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/r3f-perf": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/r3f-perf/-/r3f-perf-7.2.3.tgz", + "integrity": "sha512-4+P/N/bnO9D8nzdm3suL/NjPZK/HHdjwpvajhi8j7eB41i2ECN6lX9RXiKSpHzpsDi2ui1tBj6q7/sz5opoqXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-icons": "^1.3.0", + "@react-three/drei": "^9.103.0", + "@stitches/react": "^1.2.8", + "@utsubo/events": "^0.1.7", + "zustand": "~4.5.2" + }, + "peerDependencies": { + "@react-three/fiber": ">=8.0", + "react": ">=18.0", + "react-dom": ">=18.0", + "three": ">=0.133" + }, + "peerDependenciesMeta": { + "@react-three/fiber": { + "optional": true + }, + "dom": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/r3f-perf/node_modules/@mediapipe/tasks-vision": { + "version": "0.10.17", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", + "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==", + "license": "Apache-2.0" + }, + "node_modules/r3f-perf/node_modules/@react-spring/animated": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", + "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", + "license": "MIT", + "dependencies": { + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/r3f-perf/node_modules/@react-spring/core": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", + "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/r3f-perf/node_modules/@react-spring/rafz": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", + "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==", + "license": "MIT" + }, + "node_modules/r3f-perf/node_modules/@react-spring/shared": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", + "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", + "license": "MIT", + "dependencies": { + "@react-spring/rafz": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/r3f-perf/node_modules/@react-spring/three": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.7.5.tgz", + "integrity": "sha512-RxIsCoQfUqOS3POmhVHa1wdWS0wyHAUway73uRLp3GAL5U2iYVNdnzQsep6M2NZ994BlW8TcKuMtQHUqOsy6WA==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/core": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "@react-three/fiber": ">=6.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "three": ">=0.126" + } + }, + "node_modules/r3f-perf/node_modules/@react-spring/types": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", + "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", + "license": "MIT" + }, + "node_modules/r3f-perf/node_modules/@react-three/drei": { + "version": "9.122.0", + "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-9.122.0.tgz", + "integrity": "sha512-SEO/F/rBCTjlLez7WAlpys+iGe9hty4rNgjZvgkQeXFSiwqD4Hbk/wNHMAbdd8vprO2Aj81mihv4dF5bC7D0CA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mediapipe/tasks-vision": "0.10.17", + "@monogrid/gainmap-js": "^3.0.6", + "@react-spring/three": "~9.7.5", + "@use-gesture/react": "^10.3.1", + "camera-controls": "^2.9.0", + "cross-env": "^7.0.3", + "detect-gpu": "^5.0.56", + "glsl-noise": "^0.0.0", + "hls.js": "^1.5.17", + "maath": "^0.10.8", + "meshline": "^3.3.1", + "react-composer": "^5.0.3", + "stats-gl": "^2.2.8", + "stats.js": "^0.17.0", + "suspend-react": "^0.1.3", + "three-mesh-bvh": "^0.7.8", + "three-stdlib": "^2.35.6", + "troika-three-text": "^0.52.0", + "tunnel-rat": "^0.1.2", + "utility-types": "^3.11.0", + "zustand": "^5.0.1" + }, + "peerDependencies": { + "@react-three/fiber": "^8", + "react": "^18", + "react-dom": "^18", + "three": ">=0.137" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/r3f-perf/node_modules/@react-three/drei/node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, + "node_modules/r3f-perf/node_modules/troika-three-text": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz", + "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==", + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.2", + "troika-three-utils": "^0.52.4", + "troika-worker-utils": "^0.52.0", + "webgl-sdf-generator": "1.1.1" + }, + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/r3f-perf/node_modules/troika-three-utils": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz", + "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/r3f-perf/node_modules/troika-worker-utils": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", + "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==", + "license": "MIT" + }, + "node_modules/r3f-perf/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/raf": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", @@ -21730,9 +22145,11 @@ "integrity": "sha512-xfCYj4RnlozReCmUd+XQzj6/5OjDNHBy5nT6rVwrOKGENAvpXe2z1jL+DZYaMu4/9pNsjH/4Os/VvS9IrH7IOQ==" }, "node_modules/three-mesh-bvh": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.6.tgz", - "integrity": "sha512-rCjsnxEqR9r1/C/lCqzGLS67NDty/S/eT6rAJfDvsanrIctTWdNoR4ZOGWewCB13h1QkVo2BpmC0wakj1+0m8A==", + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.8.tgz", + "integrity": "sha512-BGEZTOIC14U0XIRw3tO4jY7IjP7n7v24nv9JXS1CyeVRWOCkcOMhRnmENUjuV39gktAw4Ofhr0OvIAiTspQrrw==", + "deprecated": "Deprecated due to three.js version incompatibility. Please use v0.8.0, instead.", + "license": "MIT", "peerDependencies": { "three": ">= 0.151.0" } diff --git a/package.json b/package.json index 67e0492..8d66fe9 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "type": "git", "url": "git+https://github.com/pholmq/TSN.git" }, - "version": "1.0.3", + "version": "1.0.4", "author": "Patrik Holmqvist", "description": "The Tychosium www.tychos.space", "license": "GPL-2.0-or-only", @@ -15,10 +15,15 @@ "@react-three/fiber": "8.15.19", "@react-three/postprocessing": "2.19.0", "@types/three": "0.171.0", + "file-saver": "^2.0.5", "fuse.js": "^7.1.0", "leva": "0.9.34", "lodash-es": "^4.17.21", + "mediabunny": "^1.37.0", + "mobx": "^6.15.0", + "mobx-react": "^9.2.1", "postprocessing": "6.35.2", + "r3f-perf": "^7.2.3", "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "5.4.0", diff --git a/public/textures/planets/2k_pluto.jpg b/public/textures/planets/2k_pluto.jpg new file mode 100644 index 0000000..d3a2c65 Binary files /dev/null and b/public/textures/planets/2k_pluto.jpg differ diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md new file mode 100644 index 0000000..8d2cea9 --- /dev/null +++ b/src/CHANGELOG.md @@ -0,0 +1,7 @@ +# Change Log + +## [1.0.3] - 2026-01-27 +### Added +- Ephemerides +- HA Reys Star constellations +- diff --git a/src/TSNext.jsx b/src/TSNext.jsx index a7e756d..592c61c 100644 --- a/src/TSNext.jsx +++ b/src/TSNext.jsx @@ -1,3 +1,4 @@ +// src/TSNext.jsx import "./index.css"; import { useEffect, Suspense, useState } from "react"; import { Canvas } from "@react-three/fiber"; @@ -10,11 +11,14 @@ import PlotSolarSystem from "./components/PlotSolarSystem"; import TraceController from "./components/Trace/TraceController"; import LightEffectsMenu from "./components/Menus/LightEffectsMenu"; import PlanetsPositionsMenu from "./components/Menus/PlanetsPositionsMenu"; -import StarsHelpersMenu from "./components/Menus/StarsHelpersMenu"; import PosController from "./components/PosController"; import Positions from "./components/Menus/Positions"; -import Ephemerides from "./components/Menus/Ephemerides"; -import EphController from "./components/EphController"; +import Ephemerides from "./components/Ephemerides/Ephemerides"; +import EphController from "./components/Ephemerides/EphController"; +import EphemeridesResult from "./components/Ephemerides/EphemeridesResult"; +import EphemeridesProgress from "./components/Ephemerides/EphemeridesProgress"; +import Plot from "./components/Plot/Plot"; +// import PlotController from "./components/Plot/PlotController"; import Stars from "./components/Stars/Stars"; import LabeledStars from "./components/Stars/LabeledStars"; // import BSCStars from "./components/Stars/BSCStars"; @@ -32,7 +36,11 @@ import HighlightSelectedStar from "./components/StarSearch/HighlightSelectedStar import Help from "./components/Help/Help"; import PlanetCameraCompass from "./components/PlanetCamera/PlanetCameraCompass"; import TransitionCamera from "./components/PlanetCamera/TransitionCamera"; -import Constellations from"./components/Stars/Constellations"; +import Constellations from "./components/Stars/Constellations"; +import PlanetCameraHelper from "./components/PlanetCamera/PlanetCameraHelper"; +import { VideoCanvas } from "./components/Recorder/r3f-video-recorder"; +import RecorderMenu from "./components/Menus/RecorderMenu"; +import RecorderController from "./components/Recorder/RecorderController"; const isTouchDevice = () => { return ( @@ -67,6 +75,7 @@ const TSNext = () => { const searchStars = useStore((s) => s.searchStars); const planetCamera = useStore((s) => s.planetCamera); const cameraTransitioning = useStore((s) => s.cameraTransitioning); + const showPerf = useStore((s) => s.showPerf); const isTouchDev = isTouchDevice(); @@ -103,20 +112,31 @@ const TSNext = () => { + + + + {BSCStarsOn && !isTouchDev && searchStars && } - - {/* IntroQuote is always rendered and visible */} + {/* Other components wrapped in Suspense */} @@ -132,8 +152,8 @@ const TSNext = () => { + {/* */} - @@ -143,8 +163,9 @@ const TSNext = () => { {BSCStarsOn && !isTouchDev && } + - + ); }; diff --git a/src/components/AnimationController.jsx b/src/components/AnimationController.jsx index 1f732bf..8e73f95 100644 --- a/src/components/AnimationController.jsx +++ b/src/components/AnimationController.jsx @@ -1,5 +1,6 @@ import { useStore } from "../store"; import { useFrame, useThree } from "@react-three/fiber"; +import { useVideoCanvas } from "./Recorder/r3f-video-recorder"; //Note: UserInterface handles most of this but we need to have the useFrame hook //within the canvas component @@ -7,20 +8,26 @@ import { useFrame, useThree } from "@react-three/fiber"; const AnimationController = () => { useStore((s) => s.updAC); //Triggers a rerender when needed + const videoCanvas = useVideoCanvas(); // Tap into the deterministic clock + const { invalidate, clock } = useThree(); const posRef = useStore((s) => s.posRef); if (posRef.current == null) posRef.current = 0; const run = useStore((s) => s.run); const speedFact = useStore((s) => s.speedFact); const speedMultiplier = useStore((s) => s.speedMultiplier); - invalidate(); - clock.getDelta(); //Reset delta so that it's 0 when run becomes true + + // Priority -1 ensures this runs BEFORE components (like planets) read the position useFrame((state, delta) => { + // If actively recording, force a perfect frame step. Otherwise, use real hardware delta. + const isRecording = videoCanvas?.recording !== null; + const activeDelta = isRecording ? 1 / videoCanvas.fps : delta; + if (run) { - posRef.current = posRef.current + delta * (speedFact * speedMultiplier); - invalidate(); //Ivalidate frame so we get a render since we have frameloop=demand + posRef.current = + posRef.current + activeDelta * (speedFact * speedMultiplier); } - }); + }, -1); return null; }; diff --git a/src/components/EphController.jsx b/src/components/EphController.jsx deleted file mode 100644 index 2bab03e..0000000 --- a/src/components/EphController.jsx +++ /dev/null @@ -1,134 +0,0 @@ -import { useEffect, useState } from "react"; -import { useThree } from "@react-three/fiber"; -import { useStore, usePlotStore, useSettingsStore } from "../store"; -import { useEphemeridesStore } from "./Menus/ephemeridesStore"; -import useFrameInterval from "../utils/useFrameInterval"; -import { - posToDate, - posToTime, - dateTimeToPos, - sDay, -} from "../utils/time-date-functions"; -import { - movePlotModel, - getPlotModelRaDecDistance, -} from "../utils/plotModelFunctions"; - -// --- Helper function to save file --- -const saveEphemeridesAsText = (data, params) => { - let output = "--- EPHEMERIDES REPORT ---\n"; - output += `Generated on: ${new Date().toLocaleString()}\n`; - output += `Start Date: ${params.startDate}\n`; - output += `End Date: ${params.endDate}\n`; - output += `Step Size: ${params.stepSize} ${params.stepFactor === 1 ? "Days" : "Years"}\n`; - output += "--------------------------------------\n\n"; - - Object.keys(data).forEach((planetName) => { - output += `PLANET: ${planetName.toUpperCase()}\n`; - // Table Header - output += `${"Date".padEnd(12)} | ${"Time".padEnd(10)} | ${"RA".padEnd(12)} | ${"Dec".padEnd(12)} | ${"Dist".padEnd(12)} | ${"Elongation".padEnd(10)}\n`; - output += "-".repeat(80) + "\n"; - - // Table Rows - data[planetName].forEach((row) => { - output += `${row.date.padEnd(12)} | ${row.time.padEnd(10)} | ${row.ra.padEnd(12)} | ${row.dec.padEnd(12)} | ${row.dist.padEnd(12)} | ${row.elong}\n`; - }); - output += "\n" + "=".repeat(80) + "\n\n"; - }); - - // Create Blob - const blob = new Blob([output], { type: "text/plain" }); - const url = URL.createObjectURL(blob); - - // Generate Filename from Start and End Dates - // We replace potential unsafe characters just in case, though standard YYYY-MM-DD is usually safe - const safeStart = params.startDate.replace(/[:/]/g, "-"); - const safeEnd = params.endDate.replace(/[:/]/g, "-"); - const filename = `Ephemerides_${safeStart}_to_${safeEnd}.txt`; - - // Trigger Download - const link = document.createElement("a"); - link.href = url; - link.download = filename; - document.body.appendChild(link); - link.click(); - - // Cleanup - document.body.removeChild(link); - URL.revokeObjectURL(url); -}; - -const EphController = () => { - const { scene } = useThree(); - const plotObjects = usePlotStore((s) => s.plotObjects); - - const { trigger, params, resetTrigger } = useEphemeridesStore(); - - const [done, setDone] = useState(true); - - useEffect(() => { - if (trigger && params) { - const startPos = dateTimeToPos(params.startDate, "00:00:00"); - const endPos = dateTimeToPos(params.endDate, "00:00:00"); - if (params.checkedPlanets.length > 0 && startPos <= endPos) { - setDone(false); - } - resetTrigger(); - } - }, [trigger, params, resetTrigger]); - - useFrameInterval(() => { - if (done) return; - - const { startDate, endDate, stepSize, stepFactor, checkedPlanets } = params; - - const startPos = dateTimeToPos(startDate, "00:00:00"); - const endPos = dateTimeToPos(endDate, "00:00:00"); - const increment = stepSize * stepFactor; - - const ephemeridesData = {}; - checkedPlanets.forEach((planet) => { - ephemeridesData[planet] = []; - }); - - let currentPos = startPos; - let steps = 0; - const MAX_STEPS = 50000; - - // console.log("--- Starting Ephemerides Generation ---"); - - while (currentPos <= endPos && steps < MAX_STEPS) { - const currentDate = posToDate(currentPos); - const currentTime = posToTime(currentPos); - - movePlotModel(plotObjects, currentPos); - - checkedPlanets.forEach((name) => { - const data = getPlotModelRaDecDistance(name, plotObjects, scene); - - if (data) { - ephemeridesData[name].push({ - date: currentDate, - time: currentTime, - ra: data.ra, - dec: data.dec, - dist: data.dist, - elong: data.elongation, - }); - } - }); - - currentPos += increment; - steps++; - } - setDone(true); - - // console.log(`Generation Complete. Steps: ${steps}`); - - // Trigger the save file dialog with the new naming convention - saveEphemeridesAsText(ephemeridesData, params); - }); - - return null; -}; -export default EphController; \ No newline at end of file diff --git a/src/components/Ephemerides/EphController.jsx b/src/components/Ephemerides/EphController.jsx new file mode 100644 index 0000000..d6dedbb --- /dev/null +++ b/src/components/Ephemerides/EphController.jsx @@ -0,0 +1,149 @@ +import { useEffect, useState, useRef } from "react"; +import { useThree, useFrame } from "@react-three/fiber"; +import { usePlotStore } from "../../store"; +import { useEphemeridesStore } from "./ephemeridesStore"; +import { + posToDate, + posToTime, + dateTimeToPos, +} from "../../utils/time-date-functions"; +import { + movePlotModel, + getPlotModelRaDecDistance, +} from "../../utils/plotModelFunctions"; + +const EphController = () => { + const { scene, invalidate } = useThree(); // invalidate is grabbed here + const plotObjects = usePlotStore((s) => s.plotObjects); + + const { + trigger, + params, + resetTrigger, + setGeneratedData, + setGenerationError, + setIsGenerating, + setProgress, + } = useEphemeridesStore(); + + const [generating, setGenerating] = useState(false); + + const jobRef = useRef({ + startPos: 0, + currentStep: 0, + totalSteps: 0, + increment: 0, + checkedPlanets: [], + data: {}, + lastProgress: 0, + }); + + // 1. Initialize Job + useEffect(() => { + if (trigger && params) { + setIsGenerating(true); + setProgress(0); + + const startPos = dateTimeToPos(params.startDate, "00:00:00"); + const endPos = dateTimeToPos(params.endDate, "00:00:00"); + let increment = params.stepSize * params.stepFactor; + + // Reverse direction if Start > End + if (startPos > endPos) { + increment = -increment; + } + + const totalSteps = Math.round((endPos - startPos) / increment); + + // Initialize Data Structure + const initialData = {}; + params.checkedPlanets.forEach((planet) => { + initialData[planet] = []; + }); + + // Setup Job + jobRef.current = { + startPos: startPos, + currentStep: 0, + totalSteps: totalSteps, + increment: increment, + checkedPlanets: params.checkedPlanets, + data: initialData, + lastProgress: 0, + }; + + setGenerating(true); + resetTrigger(); + } + }, [ + trigger, + params, + resetTrigger, + setGenerationError, + setIsGenerating, + setProgress, + ]); + + // 2. Process Job in Chunks + useFrame(() => { + // Check for Cancellation + if (generating && !useEphemeridesStore.getState().isGenerating) { + setGenerating(false); + return; + } + + if (!generating) return; + + // Keep the loop alive while generating + invalidate(); + + const job = jobRef.current; + const BATCH_SIZE = 50; + let batchCount = 0; + + while (job.currentStep <= job.totalSteps && batchCount < BATCH_SIZE) { + const currentPos = job.startPos + job.currentStep * job.increment; + const currentDate = posToDate(currentPos); + const currentTime = posToTime(currentPos); + + movePlotModel(plotObjects, currentPos); + + job.checkedPlanets.forEach((name) => { + const data = getPlotModelRaDecDistance(name, plotObjects, scene); + if (data) { + job.data[name].push({ + date: currentDate, + time: currentTime, + ra: data.ra, + dec: data.dec, + dist: data.dist, + elong: data.elongation, + }); + } + }); + + job.currentStep++; + batchCount++; + } + + const progress = Math.min( + 100, + Math.floor((job.currentStep / (job.totalSteps + 1)) * 100) + ); + + if (progress > job.lastProgress) { + setProgress(progress); + job.lastProgress = progress; + } + + // Completion Check + if (job.currentStep > job.totalSteps) { + setGeneratedData(job.data); + setGenerating(false); + setProgress(100); + } + }); + + return null; +}; +export default EphController; diff --git a/src/components/Ephemerides/Ephemerides.jsx b/src/components/Ephemerides/Ephemerides.jsx new file mode 100644 index 0000000..d6231ea --- /dev/null +++ b/src/components/Ephemerides/Ephemerides.jsx @@ -0,0 +1,226 @@ +import { useEffect, useRef } from "react"; +import { createPortal } from "react-dom"; +import { useControls, useCreateStore, Leva, button } from "leva"; +import { useStore, useSettingsStore } from "../../store"; +import { + isValidDate, + posToDate, + speedFactOpts, + sDay, +} from "../../utils/time-date-functions"; +import { useEphemeridesStore } from "./ephemeridesStore"; + +const Ephemerides = () => { + const { ephimerides, setEphemerides, posRef } = useStore(); + const { settings } = useSettingsStore(); + + const setGenerationParams = useEphemeridesStore((s) => s.setGenerationParams); + const setGenerationError = useEphemeridesStore((s) => s.setGenerationError); + const isGenerating = useEphemeridesStore((s) => s.isGenerating); + + const levaEphStore = useCreateStore(); + + const valuesRef = useRef({ + "Start Date": posToDate(posRef.current), + "End Date": posToDate(posRef.current), + "Step size": 1, + "\u{000D}": sDay, + }); + + const checkboxes = {}; + settings.forEach((s) => { + if (s.type === "planet" && s.name !== "Earth") { + if (valuesRef.current[s.name] === undefined) { + valuesRef.current[s.name] = false; + } + checkboxes[s.name] = { + value: false, + onChange: (v) => { + valuesRef.current[s.name] = v; + }, + }; + } + }); + + const handleCreate = () => { + if (isGenerating) return; + + const formValues = valuesRef.current; + + const checkedPlanets = settings + .filter((s) => s.type === "planet" && s.name !== "Earth") + .filter((s) => formValues[s.name] === true) + .map((s) => s.name); + + if (checkedPlanets.length === 0) { + setGenerationError( + "No planets selected.\nPlease select at least one planet to generate data." + ); + return; + } + + setGenerationParams({ + startDate: formValues["Start Date"], + endDate: formValues["End Date"], + stepSize: formValues["Step size"], + stepFactor: formValues["\u{000D}"], + checkedPlanets, + }); + }; + + const handleInvalidInput = (field, fallbackValue) => { + levaEphStore.set({ [field]: fallbackValue }); + valuesRef.current[field] = fallbackValue; + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur(); + } + }; + + useEffect(() => { + if (ephimerides) { + const currentDate = posToDate(posRef.current); + + valuesRef.current["Start Date"] = currentDate; + valuesRef.current["End Date"] = currentDate; + + levaEphStore.set({ + "Start Date": currentDate, + "End Date": currentDate, + }); + } + }, [ephimerides, posRef, levaEphStore]); + + // Highly targeted DOM injection for the X button + useEffect(() => { + if (!ephimerides) return; + + const interval = setInterval(() => { + // Find the deepest div containing ONLY the exact title text + const textDiv = Array.from(document.querySelectorAll("div")).find( + (el) => + el.textContent.trim() === "Ephemerides" && el.children.length === 0 + ); + + if (textDiv) { + // Leva's title bar is the immediate flex container wrapping this text + const titleBar = textDiv.parentElement; + + if (titleBar && !titleBar.querySelector(".leva-close-x")) { + // Allow the title bar to anchor our absolutely positioned button + titleBar.style.position = "relative"; + + const closeBtn = document.createElement("div"); + closeBtn.className = "leva-close-x"; + closeBtn.innerHTML = "✕"; + + // Style it seamlessly into the top right corner + Object.assign(closeBtn.style, { + position: "absolute", + right: "12px", + top: "50%", + transform: "translateY(-50%)", + cursor: "pointer", + color: "#8C92A4", + fontSize: "14px", + fontWeight: "bold", + padding: "4px", + zIndex: "9999", + }); + + // Native hover colors + closeBtn.onmouseenter = () => (closeBtn.style.color = "#FFFFFF"); + closeBtn.onmouseleave = () => (closeBtn.style.color = "#8C92A4"); + + // CRITICAL: Stop the click from passing through and triggering Leva's drag feature + closeBtn.onmousedown = (e) => e.stopPropagation(); + + // Close action + closeBtn.onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + setEphemerides(false); + }; + + titleBar.appendChild(closeBtn); + } + } + }, 150); + + return () => clearInterval(interval); + }, [ephimerides, setEphemerides]); + + useControls( + { + Generate: button(handleCreate, { disabled: isGenerating }), + "Start Date": { + value: posToDate(posRef.current), + onChange: (v) => { + valuesRef.current["Start Date"] = v; + }, + onEditEnd: (value) => { + if (!isValidDate(value)) + handleInvalidInput("Start Date", posToDate(posRef.current)); + }, + }, + "End Date": { + value: posToDate(posRef.current), + onChange: (v) => { + valuesRef.current["End Date"] = v; + }, + onEditEnd: (value) => { + if (!isValidDate(value)) + handleInvalidInput("End Date", posToDate(posRef.current)); + }, + }, + "Step size": { + value: 1, + onChange: (v) => { + valuesRef.current["Step size"] = v; + }, + onEditEnd: (value) => { + const num = parseFloat(value); + if (isNaN(num) || num <= 0) handleInvalidInput("Step size", 1); + }, + }, + "\u{000D}": { + value: sDay, + options: speedFactOpts, + onChange: (v) => { + valuesRef.current["\u{000D}"] = v; + }, + }, + ...checkboxes, + }, + { store: levaEphStore }, + [settings, isGenerating] + ); + + if (!ephimerides) return null; + + return createPortal( +
+ +
, + document.body + ); +}; + +export default Ephemerides; diff --git a/src/components/Ephemerides/EphemeridesProgress.jsx b/src/components/Ephemerides/EphemeridesProgress.jsx new file mode 100644 index 0000000..4df27d0 --- /dev/null +++ b/src/components/Ephemerides/EphemeridesProgress.jsx @@ -0,0 +1,100 @@ +import React from "react"; +import { createPortal } from "react-dom"; +import { useEphemeridesStore } from "./ephemeridesStore"; + +const EphemeridesProgress = () => { + const isGenerating = useEphemeridesStore((s) => s.isGenerating); + const progress = useEphemeridesStore((s) => s.progress); + const cancelGeneration = useEphemeridesStore((s) => s.cancelGeneration); + + if (!isGenerating) return null; + + return createPortal( +
+
+

+ Generating Ephemerides data +

+ + {progress}% + +
+ + {/* Progress Bar Container */} +
+
+
+ + {/* Cancel Button */} +
+ +
+
, + document.body + ); +}; + +export default EphemeridesProgress; diff --git a/src/components/Ephemerides/EphemeridesResult.jsx b/src/components/Ephemerides/EphemeridesResult.jsx new file mode 100644 index 0000000..d6b8f3a --- /dev/null +++ b/src/components/Ephemerides/EphemeridesResult.jsx @@ -0,0 +1,294 @@ +import React, { useState, useEffect } from "react"; +import { createPortal } from "react-dom"; +import { useEphemeridesStore } from "./ephemeridesStore"; +import { FaSave, FaExclamationTriangle } from "react-icons/fa"; +import { speedFactOpts } from "../../utils/time-date-functions"; + +const EphemeridesResult = () => { + const { showResult, generatedData, generationError, params, closeResult } = + useEphemeridesStore(); + + const [isDragging, setIsDragging] = useState(false); + const [position, setPosition] = useState({ x: 0, y: 0 }); + const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); + const [previewText, setPreviewText] = useState(""); + + // --- Formatting Logic --- + const formatDataToText = (data, parameters) => { + if (!data || !parameters) return ""; + + const displayUnit = Object.keys(speedFactOpts).find( + (key) => speedFactOpts[key] === parameters.stepFactor + ); + + let output = "--- EPHEMERIDES REPORT ---\n"; + output += `Generated on: ${new Date().toLocaleString()}\n`; + output += `Start Date: ${parameters.startDate}\n`; + output += `End Date: ${parameters.endDate}\n`; + output += `Step Size: ${parameters.stepSize} ${displayUnit}\n`; + output += "--------------------------------------\n\n"; + + Object.keys(data).forEach((planetName) => { + output += `PLANET: ${planetName.toUpperCase()}\n`; + output += `${"Date".padEnd(12)} | ${"Time".padEnd(10)} | ${"RA".padEnd( + 12 + )} | ${"Dec".padEnd(12)} | ${"Dist".padEnd(12)} | ${"Elongation".padEnd( + 10 + )}\n`; + output += "-".repeat(80) + "\n"; + + data[planetName].forEach((row) => { + output += `${row.date.padEnd(12)} | ${row.time.padEnd( + 10 + )} | ${row.ra.padEnd(12)} | ${row.dec.padEnd(12)} | ${row.dist.padEnd( + 12 + )} | ${row.elong}\n`; + }); + output += "\n" + "=".repeat(80) + "\n\n"; + }); + + return output; + }; + + useEffect(() => { + if (generatedData && params) { + setPreviewText(formatDataToText(generatedData, params)); + } + }, [generatedData, params]); + + // --- Dragging Logic --- + useEffect(() => { + const handleMouseMove = (e) => { + if (!isDragging) return; + setPosition({ + x: position.x + (e.clientX - dragStart.x), + y: position.y + (e.clientY - dragStart.y), + }); + setDragStart({ x: e.clientX, y: e.clientY }); + }; + + const handleMouseUp = () => setIsDragging(false); + + if (isDragging) { + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + } + return () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + }, [isDragging, dragStart, position]); + + const handleMouseDown = (e) => { + if (e.target.closest(".popup-header")) { + setIsDragging(true); + setDragStart({ x: e.clientX, y: e.clientY }); + } + }; + + const handleSave = () => { + if (!previewText || !params) return; + const blob = new Blob([previewText], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const safeStart = params.startDate.replace(/[:/]/g, "-"); + const safeEnd = params.endDate.replace(/[:/]/g, "-"); + const filename = `Ephemerides_${safeStart}_to_${safeEnd}.txt`; + const link = document.createElement("a"); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + }; + + if (!showResult) return null; + + return createPortal( +
+ {/* Header */} +
+
+ {generationError ? ( + + ) : ( + + )} +

+ {generationError ? "Error" : "Ephemerides result"} +

+
+ +
+ + {/* Content Body */} +
+ {generationError ? ( +
+ {/*

+ Calculation Stopped +

*/} +

+ {generationError} +

+
+ ) : ( +
+

+ Click 'Save' to download as a text file. +

+