diff --git a/README.md b/README.md index 24f53cb..4ce177d 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Any questions, feedback or suggestions are welcome at [EVE PI Discord](https://d ## [Avanto hosted PI tool](https://pi.avanto.tk) ![Screenshot of PI tool](https://github.com/calli-eve/eve-pi/blob/main/images/eve-pi.png) +![3D render of a planet](https://github.com/calli-eve/eve-pi/blob/main/images/3dplanet.png) Features: @@ -17,6 +18,7 @@ Features: - Highlight the planet if extractor has stopped or has not been started. - Backup to download characters to a file - Rstore from a file. Must be from the same instance! +- View the 3D render of the planet with your PI setup by clicking the planet ## Basic usage @@ -73,3 +75,12 @@ EVE_SSO_CALLBACK_URL=Callback URL (This should be the domain you are hosting at ## Hosting Easiest way to host is deploy the app through Vercel https://vercel.com. Login with github, point to eve-pi repository, setup the env variables and the app should work out of the box. + +## Planetary coordinate system + +ESI PI planet info endpoint returns pin coorinates. These coordinates seem to work like this: + +Latitude starts from 0 at the north pole and ends at Pi (3.141...) at the south pole. +Longitude starts at some point from 0 and ends at Tau (6.283...) after going around the planet. + +To translate the coordinates to 2D plane one could use Azimuthal equidistant projection. With this service we will just render a webgl sphere and place the pins directly on it. To access the render click the planet icon. diff --git a/images/3dplanet.png b/images/3dplanet.png new file mode 100644 index 0000000..d6e9529 Binary files /dev/null and b/images/3dplanet.png differ diff --git a/package-lock.json b/package-lock.json index 0c68e8d..f9cd200 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,11 +27,13 @@ "react-countdown": "^2.3.5", "react-dom": "18.2.0", "sharp": "^0.32.1", + "three": "^0.154.0", "typescript": "5.1.3" }, "devDependencies": { "@types/crypto-js": "^4.1.1", - "@types/luxon": "^3.3.0" + "@types/luxon": "^3.3.0", + "@types/three": "^0.152.1" } }, "node_modules/@babel/code-frame": { @@ -870,6 +872,12 @@ "tslib": "^2.4.0" } }, + "node_modules/@tweenjs/tween.js": { + "version": "18.6.4", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-18.6.4.tgz", + "integrity": "sha512-lB9lMjuqjtuJrx7/kOkqQBtllspPIN+96OvTCeJ2j5FEzinoAXTdAMFnDAQT1KVPRlnYfBrqxtqP66vDM40xxQ==", + "dev": true + }, "node_modules/@types/crypto-js": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.1.tgz", @@ -941,6 +949,31 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, + "node_modules/@types/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-9w+a7bR8PeB0dCT/HBULU2fMqf6BAzvKbxFboYhmDtDkKPiyXYbjoe2auwsXlEFI7CFNMF1dCv3dFH5Poy9R1w==", + "dev": true + }, + "node_modules/@types/three": { + "version": "0.152.1", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.152.1.tgz", + "integrity": "sha512-PMOCQnx9JRmq+2OUGTPoY9h1hTWD2L7/nmuW/SyNq1Vbq3Lwt3MNdl3wYSa4DvLTGv62NmIXD9jYdAOwohwJyw==", + "dev": true, + "dependencies": { + "@tweenjs/tween.js": "~18.6.4", + "@types/stats.js": "*", + "@types/webxr": "*", + "fflate": "~0.6.9", + "lil-gui": "~0.17.0" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.2.tgz", + "integrity": "sha512-szL74BnIcok9m7QwYtVmQ+EdIKwbjPANudfuvDrAF8Cljg9MKUlIoc1w5tjj9PMpeSH3U1Xnx//czQybJ0EfSw==", + "dev": true + }, "node_modules/@typescript-eslint/parser": { "version": "5.59.11", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.11.tgz", @@ -2377,6 +2410,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "dev": true + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3201,6 +3240,12 @@ "node": ">= 0.8.0" } }, + "node_modules/lil-gui": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/lil-gui/-/lil-gui-0.17.0.tgz", + "integrity": "sha512-MVBHmgY+uEbmJNApAaPbtvNh1RCAeMnKym82SBjtp5rODTYKWtM+MXHCifLe2H2Ti1HuBGBtK/5SyG4ShQ3pUQ==", + "dev": true + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -4556,6 +4601,11 @@ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" }, + "node_modules/three": { + "version": "0.154.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.154.0.tgz", + "integrity": "sha512-Uzz8C/5GesJzv8i+Y2prEMYUwodwZySPcNhuJUdsVMH2Yn4Nm8qlbQe6qRN5fOhg55XB0WiLfTPBxVHxpE60ug==" + }, "node_modules/titleize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", @@ -5371,6 +5421,12 @@ "tslib": "^2.4.0" } }, + "@tweenjs/tween.js": { + "version": "18.6.4", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-18.6.4.tgz", + "integrity": "sha512-lB9lMjuqjtuJrx7/kOkqQBtllspPIN+96OvTCeJ2j5FEzinoAXTdAMFnDAQT1KVPRlnYfBrqxtqP66vDM40xxQ==", + "dev": true + }, "@types/crypto-js": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.1.tgz", @@ -5442,6 +5498,31 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, + "@types/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-9w+a7bR8PeB0dCT/HBULU2fMqf6BAzvKbxFboYhmDtDkKPiyXYbjoe2auwsXlEFI7CFNMF1dCv3dFH5Poy9R1w==", + "dev": true + }, + "@types/three": { + "version": "0.152.1", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.152.1.tgz", + "integrity": "sha512-PMOCQnx9JRmq+2OUGTPoY9h1hTWD2L7/nmuW/SyNq1Vbq3Lwt3MNdl3wYSa4DvLTGv62NmIXD9jYdAOwohwJyw==", + "dev": true, + "requires": { + "@tweenjs/tween.js": "~18.6.4", + "@types/stats.js": "*", + "@types/webxr": "*", + "fflate": "~0.6.9", + "lil-gui": "~0.17.0" + } + }, + "@types/webxr": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.2.tgz", + "integrity": "sha512-szL74BnIcok9m7QwYtVmQ+EdIKwbjPANudfuvDrAF8Cljg9MKUlIoc1w5tjj9PMpeSH3U1Xnx//czQybJ0EfSw==", + "dev": true + }, "@typescript-eslint/parser": { "version": "5.59.11", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.11.tgz", @@ -6443,6 +6524,12 @@ "reusify": "^1.0.4" } }, + "fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "dev": true + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -6999,6 +7086,12 @@ "type-check": "~0.4.0" } }, + "lil-gui": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/lil-gui/-/lil-gui-0.17.0.tgz", + "integrity": "sha512-MVBHmgY+uEbmJNApAaPbtvNh1RCAeMnKym82SBjtp5rODTYKWtM+MXHCifLe2H2Ti1HuBGBtK/5SyG4ShQ3pUQ==", + "dev": true + }, "lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -7878,6 +7971,11 @@ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" }, + "three": { + "version": "0.154.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.154.0.tgz", + "integrity": "sha512-Uzz8C/5GesJzv8i+Y2prEMYUwodwZySPcNhuJUdsVMH2Yn4Nm8qlbQe6qRN5fOhg55XB0WiLfTPBxVHxpE60ug==" + }, "titleize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", diff --git a/package.json b/package.json index 7f3389d..4b4d586 100644 --- a/package.json +++ b/package.json @@ -29,10 +29,12 @@ "react-countdown": "^2.3.5", "react-dom": "18.2.0", "sharp": "^0.32.1", + "three": "^0.154.0", "typescript": "5.1.3" }, "devDependencies": { "@types/crypto-js": "^4.1.1", - "@types/luxon": "^3.3.0" + "@types/luxon": "^3.3.0", + "@types/three": "^0.152.1" } } diff --git a/public/circle.svg b/public/circle.svg new file mode 100644 index 0000000..77ebcff --- /dev/null +++ b/public/circle.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/public/factory.png b/public/factory.png new file mode 100644 index 0000000..6bc5628 Binary files /dev/null and b/public/factory.png differ diff --git a/src/app/components/PlanetaryInteraction/PinsCanvas3D.tsx b/src/app/components/PlanetaryInteraction/PinsCanvas3D.tsx new file mode 100644 index 0000000..438d180 --- /dev/null +++ b/src/app/components/PlanetaryInteraction/PinsCanvas3D.tsx @@ -0,0 +1,140 @@ +import React, { useEffect } from "react"; +import { PlanetInfo, PlanetInfoUniverse } from "./PlanetCard"; +import * as THREE from "three"; +import * as BufferGeometryUtils from "three/examples/jsm/utils/BufferGeometryUtils.js"; +import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; + +const commandCenterIds = [2254, 2524, 2525, 2533, 2534, 2549, 2550, 2551]; + +const PinsCanvas3D = ({ + planetInfo, +}: { + planetInfo: PlanetInfo | undefined; +}) => { + useEffect(() => { + if (!planetInfo) return; + const pinsWithoutCommandCentes = + planetInfo?.pins.filter( + (p) => !commandCenterIds.some((c) => c === p.type_id) + ) ?? []; + + const CANVAS = document.querySelector("#canvas") as HTMLCanvasElement; + + if (!CANVAS) return; + + const SCENE_ANTIALIAS = true; + const SCENE_ALPHA = true; + const SCENE_BACKGROUND_COLOR = 0x000000; + + const CAMERA_FOV = 20; + const CAMERA_NEAR = 100; + const CAMERA_FAR = 500; + const CAMERA_X = 0; + const CAMERA_Y = 0; + const CAMERA_Z = 220; + + const SPHERE_RADIUS = 30; + const LATITUDE_COUNT = 40; + const LONGITUDE_COUNT = 80; + + const DOT_SIZE = 0.2; + const DOT_COLOR = 0x36454f; + + const renderScene = () => { + const renderer = new THREE.WebGLRenderer({ + canvas: CANVAS as HTMLCanvasElement, + antialias: SCENE_ANTIALIAS, + alpha: SCENE_ALPHA, + }); + + const camera = new THREE.PerspectiveCamera( + CAMERA_FOV, + CANVAS.width / CANVAS.height, + CAMERA_NEAR, + CAMERA_FAR + ); + + const controls = new OrbitControls(camera, renderer.domElement); + camera.position.set(CAMERA_X, CAMERA_Y, CAMERA_Z); + controls.update(); + + const scene = new THREE.Scene(); + scene.background = new THREE.Color(SCENE_BACKGROUND_COLOR); + + const dotGeometries: THREE.CircleGeometry[] = []; + const dotGeometriesPI: THREE.CircleGeometry[] = []; + + const vector = new THREE.Vector3(); + const vectorPI = new THREE.Vector3(); + + pinsWithoutCommandCentes.forEach((p) => { + const dotGeometryPI = new THREE.CircleGeometry(DOT_SIZE, 9); + const phi = p.latitude; + const theta = p.longitude; + vectorPI.setFromSphericalCoords(SPHERE_RADIUS, phi, theta); + dotGeometryPI.lookAt(vectorPI); + dotGeometryPI.translate(vectorPI.x, vectorPI.y, vectorPI.z); + dotGeometriesPI.push(dotGeometryPI); + }); + + for (let lat = 0; lat < LATITUDE_COUNT; lat += 1) { + for (let lng = 0; lng < LONGITUDE_COUNT; lng += 1) { + const dotGeometry = new THREE.CircleGeometry(DOT_SIZE, 5); + const phi = (Math.PI / LATITUDE_COUNT) * lat; + const theta = ((2 * Math.PI) / LONGITUDE_COUNT) * lng; + vector.setFromSphericalCoords(SPHERE_RADIUS, phi, theta); + dotGeometry.lookAt(vector); + dotGeometry.translate(vector.x, vector.y, vector.z); + dotGeometries.push(dotGeometry); + } + } + + const mergedDotGeometries = + BufferGeometryUtils.mergeBufferGeometries(dotGeometries); + + const mergedDotGeometriesPI = + BufferGeometryUtils.mergeBufferGeometries(dotGeometriesPI); + + const dotMaterial = new THREE.MeshBasicMaterial({ + color: DOT_COLOR, + side: THREE.DoubleSide, + }); + + const dotMaterialPI = new THREE.MeshBasicMaterial({ + color: 0xfdda0d, + side: THREE.DoubleSide, + }); + const dotMesh = new THREE.Mesh(mergedDotGeometries, dotMaterial); + const dotMeshPI = new THREE.Mesh(mergedDotGeometriesPI, dotMaterialPI); + + scene.add(dotMesh); + scene.add(dotMeshPI); + + const animate = (time: number) => { + time *= 0.001; + controls.update(); + renderer.render(scene, camera); + + requestAnimationFrame(animate); + }; + + requestAnimationFrame(animate); + }; + + const setCanvasSize = () => { + CANVAS.width = window.innerWidth; + CANVAS.height = window.innerHeight; + + renderScene(); + }; + + setCanvasSize(); + + // When the window isresized, redraw the scene. + window.addEventListener("resize", setCanvasSize); + }, [planetInfo]); + + return ; +}; + +export default PinsCanvas3D; diff --git a/src/app/components/PlanetaryInteraction/PlanetCard.tsx b/src/app/components/PlanetaryInteraction/PlanetCard.tsx index e0e86b8..359b293 100644 --- a/src/app/components/PlanetaryInteraction/PlanetCard.tsx +++ b/src/app/components/PlanetaryInteraction/PlanetCard.tsx @@ -1,11 +1,20 @@ -import { Stack, Typography, styled } from "@mui/material"; +import { Stack, Tooltip, Typography, styled } from "@mui/material"; import Image from "next/image"; import { AccessToken, Planet } from "@/types"; import { Api } from "@/esi-api"; -import { useEffect, useState } from "react"; +import { forwardRef, useEffect, useState } from "react"; import { DateTime } from "luxon"; import { EXTRACTOR_TYPE_IDS } from "@/const"; import Countdown from "react-countdown"; +import PinsCanvas3D from "./PinsCanvas3D"; +import Slide from "@mui/material/Slide"; +import { TransitionProps } from "@mui/material/transitions"; +import Dialog from "@mui/material/Dialog"; +import AppBar from "@mui/material/AppBar"; +import Toolbar from "@mui/material/Toolbar"; +import IconButton from "@mui/material/IconButton"; +import CloseIcon from "@mui/icons-material/Close"; +import Button from "@mui/material/Button"; const StackItem = styled(Stack)(({ theme }) => ({ ...theme.typography.body2, @@ -16,40 +25,42 @@ const StackItem = styled(Stack)(({ theme }) => ({ alignItems: "center", })); +export interface Pin { + contents?: { + amount: number; + type_id: number; + }[]; + expiry_time?: string; + extractor_details?: { + cycle_time?: number; + head_radius?: number; + heads: { + head_id: number; + latitude: number; + longitude: number; + }[]; + product_type_id?: number; + qty_per_cycle?: number; + }; + factory_details?: { + schematic_id: number; + }; + install_time?: string; + last_cycle_start?: string; + latitude: number; + longitude: number; + pin_id: number; + schematic_id?: number; + type_id: number; +} + export interface PlanetInfo { links: { destination_pin_id: number; link_level: number; source_pin_id: number; }[]; - pins: { - contents?: { - amount: number; - type_id: number; - }[]; - expiry_time?: string; - extractor_details?: { - cycle_time?: number; - head_radius?: number; - heads: { - head_id: number; - latitude: number; - longitude: number; - }[]; - product_type_id?: number; - qty_per_cycle?: number; - }; - factory_details?: { - schematic_id: number; - }; - install_time?: string; - last_cycle_start?: string; - latitude: number; - longitude: number; - pin_id: number; - schematic_id?: number; - type_id: number; - }[]; + pins: Pin[]; routes: { content_type_id: number; destination_pin_id: number; @@ -60,6 +71,27 @@ export interface PlanetInfo { }[]; } +export interface PlanetInfoUniverse { + name: string; + planet_id: number; + position: { + x: number; + y: number; + z: number; + }; + system_id: number; + type_id: number; +} + +const Transition = forwardRef(function Transition( + props: TransitionProps & { + children: React.ReactElement; + }, + ref: React.Ref +) { + return ; +}); + export const PlanetCard = ({ planet, character, @@ -70,6 +102,21 @@ export const PlanetCard = ({ const [planetInfo, setPlanetInfo] = useState( undefined ); + + const [planetInfoUniverse, setPlanetInfoUniverse] = useState< + PlanetInfoUniverse | undefined + >(undefined); + + const [planetRenderOpen, setPlanetRenderOpen] = useState(false); + + const handle3DrenderOpen = () => { + setPlanetRenderOpen(true); + }; + + const handle3DrenderClose = () => { + setPlanetRenderOpen(false); + }; + const extractors = (planetInfo && planetInfo.pins @@ -92,17 +139,29 @@ export const PlanetCard = ({ ).data; return planetInfo; }; + + const getPlanetUniverse = async ( + planet: Planet + ): Promise => { + const api = new Api(); + const planetInfo = ( + await api.universe.getUniversePlanetsPlanetId(planet.planet_id) + ).data; + return planetInfo; + }; useEffect(() => { getPlanet(character, planet).then(setPlanetInfo); + getPlanetUniverse(planet).then(setPlanetInfoUniverse); }, [planet, character]); return ( - + {extractors.some((e) => { if (!e) return true; @@ -118,6 +177,11 @@ export const PlanetCard = ({ style={{ position: "absolute" }} /> )} +
+ {planetInfoUniverse?.name} + L{planet.upgrade_level} +
+ {extractors.map((e, idx) => { const inPast = () => { if (!e) return true; @@ -142,6 +206,32 @@ export const PlanetCard = ({ ); })} + + + + + + + + {planetInfoUniverse?.name} + + + + + +
); };