diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..f4667e2 --- /dev/null +++ b/.env.production @@ -0,0 +1,4 @@ +# .env.production + +REACT_APP_API_URL=https://cellmigration.isas.de/api +REACT_APP_UPLOAD_FOLDER=https://cellmigration.isas.de/uploads diff --git a/README.md b/README.md index 58beeac..1411fa7 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,92 @@ -# Getting Started with Create React App +# 3D Volume Viewer Application -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +This application is a 3D volume viewer designed to visualize medical imaging data of mouse body parts. The frontend is built with React and communicates with a Node.js backend that serves the volume files. Users can select different body parts and view the corresponding 3D volume files. -## Available Scripts +## Table of Contents -In the project directory, you can run: +1. [Features](#features) +2. [Installation](#installation) +3. [Usage](#usage) +4. [File Structure](#file-structure) +5. [API Endpoints](#api-endpoints) +6. [Technologies Used](#technologies-used) +7. [Contributing](#contributing) +8. [License](#license) -### `npm start` +## Features -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in your browser. +- Visualize 3D volume data in various formats (OME-TIFF, OME-ZARR, TIFF). +- Control rendering modes (Path Trace, Ray March). +- Adjust visualization parameters such as density, exposure, and gamma. +- Manipulate lighting settings and camera modes. +- Playback functionality for time-series data. +- Dynamic channel controls to adjust color and material properties. +- Set clipping regions and view bounding boxes. +- Flip volume along different axes. +- Screenshot capturing functionality. +- RESTful API for serving volume files. -The page will reload when you make changes.\ -You may also see any lint errors in the console. +## Installation -### `npm test` +### Prerequisites -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. +- Node.js (v12 or later) +- npm (v6 or later) -### `npm run build` +### Backend Setup -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. +1. Clone the repository: + ```bash + git clone https://github.com/yourusername/3d-volume-viewer.git + cd 3d-volume-viewer + ``` -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! +2. Navigate to the backend directory: + ```bash + cd server + ``` -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. +3. Install dependencies: + ```bash + npm install + ``` -### `npm run eject` +4. Start the server: + ```bash + npm start + ``` -**Note: this is a one-way operation. Once you `eject`, you can't go back!** +### Frontend Setup -If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. +1. Navigate to the frontend directory: + ```bash + cd ../client + ``` -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. +2. Install dependencies: + ```bash + npm install + ``` -You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. +3. Start the development server: + ```bash + npm start + ``` -## Learn More +4. Open your browser and navigate to `http://localhost:3000`. -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). +## Usage -To learn React, check out the [React documentation](https://reactjs.org/). +### Upload Files -### Code Splitting +1. Place your volume files in the appropriate directories under `server/uploads`. Each directory represents a different body part (e.g., `liver`, `brain`). -This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) +### View and Manipulate Volumes -### Analyzing the Bundle Size +1. Use the file selector in the frontend to choose a body part and a specific volume file. +2. Adjust rendering and visualization settings using the provided controls. +3. Use playback controls to navigate through time-series data. +4. Capture screenshots using the screenshot button. -This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) +## File Structure -### Making a Progressive Web App - -This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) - -### Advanced Configuration - -This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) - -### Deployment - -This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) - -### `npm run build` fails to minify - -This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..8cd3695 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +# Exit the script if any command fails +set -e + +# Configuration variables +PROJECT_DIR="." # Frontend root remains in 3D-Cell-Viewer +FRONTEND_DIR="$PROJECT_DIR" +WEB_SERVER_ROOT="/var/www/vhosts/lsfm.isas.de" + +# Generate a new release branch name with timestamp +RELEASE_BRANCH="release-$(date +'%Y%m%d%H%M%S')" + +echo "Creating a new release branch: $RELEASE_BRANCH" + +# Get the current branch name +CURRENT_BRANCH=$(git branch --show-current) + +# Checkout to the 'ft-create-deployment-script' branch if not already on it +if [ "$CURRENT_BRANCH" != "ft-create-deployment-script" ]; then + echo "Switching to 'ft-create-deployment-script' branch..." + git checkout ft-create-deployment-script +else + echo "Already on 'ft-create-deployment-script' branch." +fi + +# Pull the latest changes from the remote repository +echo "Pulling the latest changes from 'ft-create-deployment-script' branch..." +git pull origin ft-create-deployment-script + +# Create a new release branch from the current branch +git checkout -b "$RELEASE_BRANCH" + +# Optionally, push the new release branch to the remote repository (uncomment if needed) +# git push origin "$RELEASE_BRANCH" + +# Set environment variables for the API and file storage server +export REACT_APP_API_URL="https://cellmigration.isas.de/api" +export REACT_APP_UPLOAD_FOLDER="https://cellmigration.isas.de/uploads" + +# Create .htaccess file in the public directory +echo "Creating .htaccess file..." +cat > "$FRONTEND_DIR/public/.htaccess" << 'EOL' +# Enable rewrite engine +RewriteEngine On + +# If the requested resource exists as a file or directory, serve it directly +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d + +# Otherwise, redirect all requests to index.html +RewriteRule ^ index.html [QSA,L] + +# Set security headers +Header set X-Content-Type-Options "nosniff" +Header set X-Frame-Options "SAMEORIGIN" +Header set X-XSS-Protection "1; mode=block" +Header set Referrer-Policy "strict-origin-when-cross-origin" + +EOL + +# Proceed with the frontend deployment +echo "Starting frontend-only deployment..." + +# Move to the frontend directory and install dependencies +echo "Installing npm dependencies..." +cd "$FRONTEND_DIR" +npm install + +# Building the React Application +echo "Building and minifying the React app for production..." +NODE_ENV=production npm run build + +# Check if the web server root directory exists, if not, create it +if [ ! -d "$WEB_SERVER_ROOT" ]; then + echo "Directory $WEB_SERVER_ROOT does not exist. Creating it now..." + sudo mkdir -p "$WEB_SERVER_ROOT" + sudo chown -R www-data:www-data "$WEB_SERVER_ROOT" + sudo chmod -R 755 "$WEB_SERVER_ROOT" + echo "Directory $WEB_SERVER_ROOT created successfully." +fi + +# Deploy the built React app +echo "Copying the React build files to the web server root..." +sudo rm -rf "$WEB_SERVER_ROOT"/* +sudo cp -r "$FRONTEND_DIR/build/"* "$WEB_SERVER_ROOT" + +# Copy .htaccess file +echo "Copying .htaccess file..." +sudo cp "$FRONTEND_DIR/public/.htaccess" "$WEB_SERVER_ROOT/.htaccess" + +# Set correct ownership and permissions +echo "Changing ownership of the web server root directory to www-data user..." +sudo chown -R www-data:www-data "$WEB_SERVER_ROOT" + +echo "Frontend deployment completed successfully." diff --git a/package-lock.json b/package-lock.json index 2f00437..c71862a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,29 @@ "name": "3d-volume-app", "version": "0.1.0", "dependencies": { + "@aics/volume-viewer": "^3.11.2", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "antd": "^5.20.0", + "axios": "^1.7.7", + "d3": "^7.9.0", + "dat.gui": "^0.7.9", + "lucide-react": "^0.456.0", "react": "^18.3.1", + "react-color": "^2.19.3", "react-dom": "^18.3.1", + "react-rnd": "^10.4.13", + "react-router-dom": "^6.27.0", "react-scripts": "5.0.1", - "web-vitals": "^2.1.4" + "three": "^0.167.1", + "web-vitals": "^2.1.4", + "yarn": "^1.22.22" + }, + "devDependencies": { + "eslint": "^8.57.1", + "prettier": "^3.3.3", + "prettier-eslint": "^16.3.0" } }, "node_modules/@adobe/css-tools": { @@ -22,6 +38,23 @@ "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==" }, + "node_modules/@aics/volume-viewer": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/@aics/volume-viewer/-/volume-viewer-3.11.2.tgz", + "integrity": "sha512-lmYgBFirjpHVjN4Wb/ywmeUQeBqQQXpWP9D606ooXDeBsPJs+bLGfEiVgE4qj+O/3nLInLMthilcH+tcqK1JfQ==", + "dependencies": { + "geotiff": "^2.0.5", + "serialize-error": "^11.0.3", + "three": "^0.144.0", + "tweakpane": "^3.1.9", + "zarrita": "^0.3.2" + } + }, + "node_modules/@aics/volume-viewer/node_modules/three": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.144.0.tgz", + "integrity": "sha512-R8AXPuqfjfRJKkYoTQcTK7A6i3AdO9++2n8ubya/GTU+fEHhYKu1ZooRSCPkx69jbnzT7dD/xEo6eROQTt2lJw==" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -45,6 +78,96 @@ "node": ">=6.0.0" } }, + "node_modules/@ant-design/colors": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.1.0.tgz", + "integrity": "sha512-MMoDGWn1y9LdQJQSHiCC20x3uZ3CwQnv9QMz6pCmJOrqdgM9YxsoVVY0wtrdXbmfSgnV0KNk6zi09NAhMR2jvg==", + "dependencies": { + "@ctrl/tinycolor": "^3.6.1" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.21.0.tgz", + "integrity": "sha512-gIilraPl+9EoKdYxnupxjHB/Q6IHNRjEXszKbDxZdsgv4sAZ9pjkCq8yanDWNvyfjp4leir2OVAJm0vxwKK8YA==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "classnames": "^2.3.1", + "csstype": "^3.1.3", + "rc-util": "^5.35.0", + "stylis": "^4.0.13" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.0.3.tgz", + "integrity": "sha512-BrztZZKuoYcJK8uEH40ylBemf/Mu/QPiDos56g2bv6eUoniQkgQHOCOvA3+pncoFO1TaS8xcUCIqGzDA0I+ZVQ==", + "dependencies": { + "@ant-design/cssinjs": "^1.21.0", + "@babel/runtime": "^7.23.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.5.tgz", + "integrity": "sha512-kzUEdptM2vbHFn+fGkgKgbfsko5TR9GlGvAj+Xa7pKSXipbsvbqPtxcUGv7vdoPHFCr6JUBZa8Rfs+QJtFZEAw==", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/icons": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.4.0.tgz", + "integrity": "sha512-QZbWC5xQYexCI5q4/fehSEkchJr5UGtvAJweT743qKUQQGs9IH2DehNLP49DJ3Ii9m9CijD2HN6fNy3WKhIFdA==", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==" + }, + "node_modules/@ant-design/react-slick": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz", + "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==", + "dependencies": { + "@babel/runtime": "^7.10.4", + "classnames": "^2.2.5", + "json2mq": "^0.2.0", + "resize-observer-polyfill": "^1.5.1", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", @@ -2280,6 +2403,24 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -2366,20 +2507,20 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "deprecated": "Use @eslint/config-array instead", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -2405,6 +2546,14 @@ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "deprecated": "Use @eslint/object-schema instead" }, + "node_modules/@icons/material": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", + "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3245,6 +3394,11 @@ "node": ">= 8" } }, + "node_modules/@petamoriken/float16": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.8.7.tgz", + "integrity": "sha512-/Ri4xDDpe12NT6Ex/DRgHzLlobiQXEW/hmG08w1wj/YU7hLemk97c+zHQFp0iZQ9r7YqgLEXZR2sls4HxBf9NA==" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3301,6 +3455,154 @@ } } }, + "node_modules/@rc-component/async-validator": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz", + "integrity": "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.0.tgz", + "integrity": "sha512-52z3XqUwUr0+Br3B8RjN2GfuR1Pk3MZPAVd34WptWFEOyTz7OQmmn8nqgXUBOYwZem8jXp6G3iv+6Dm1+1epJA==", + "dependencies": { + "@ant-design/fast-color": "^2.0.1", + "@babel/runtime": "^7.23.6", + "classnames": "^2.2.6", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/context": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz", + "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz", + "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz", + "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", + "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.0.0.tgz", + "integrity": "sha512-L+rZ4HXP2sJ1gHMGHjsg9jlYBX/SLN2D6OxP9Zn3qgtpMWtO2vUfxVFwiogHpAIqs54FnALxraUy/BCO1yRIgg==", + "dependencies": { + "@babel/runtime": "^7.24.7", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.0.tgz", + "integrity": "sha512-h6hyILDwL+In9GAgRobwRWihLqqsD7Uft3fZGrJ7L4EiyCoxbnNYwzPXDfz7vNDhWeVyvAWQJj9fJCzpI4+b4g==", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/portal": "^1.0.0-9", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/trigger": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.2.0.tgz", + "integrity": "sha512-QarBCji02YE9aRFhZgRZmOpXBj0IZutRippsVBv85sxvG4FGk/vRxwAlkn3MS9zK5mwbETd86mAVg2tKqTkdJA==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@rc-component/portal": "^1.1.0", + "classnames": "^2.3.2", + "rc-motion": "^2.0.0", + "rc-resize-observer": "^1.3.1", + "rc-util": "^5.38.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@remix-run/router": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz", + "integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -4867,6 +5169,40 @@ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, + "node_modules/@zarrita/core": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@zarrita/core/-/core-0.0.3.tgz", + "integrity": "sha512-fWv51b+xbYnws1pkNDPwJQoDa76aojxplHyMup82u11UAiet3gURMsrrkhM6YbeTgSY1A8oGxDOrvar3SiZpLA==", + "dependencies": { + "@zarrita/storage": "^0.0.2", + "@zarrita/typedarray": "^0.0.1", + "numcodecs": "^0.2.2" + } + }, + "node_modules/@zarrita/indexing": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@zarrita/indexing/-/indexing-0.0.3.tgz", + "integrity": "sha512-Q61d9MYX6dsK1DLltEpwx4mJWCZHj0TXiaEN4QpxNDtToa/EoyytP/pYHPypO4GXBscZooJ6eZkKT5FMx9PVfg==", + "dependencies": { + "@zarrita/core": "^0.0.3", + "@zarrita/storage": "^0.0.2", + "@zarrita/typedarray": "^0.0.1" + } + }, + "node_modules/@zarrita/storage": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@zarrita/storage/-/storage-0.0.2.tgz", + "integrity": "sha512-uFt4abAoiOYLroalNDAnVaQxA17zGKrQ0waYKmTVm+bNonz8ggKZP+0FqMhgUZITGChqoANHuYTazbuU5AFXWA==", + "dependencies": { + "reference-spec-reader": "^0.2.0", + "unzipit": "^1.3.6" + } + }, + "node_modules/@zarrita/typedarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@zarrita/typedarray/-/typedarray-0.0.1.tgz", + "integrity": "sha512-ZdvNjYP1bEuQXoSTVkemV99w42jHYrJ3nh9golCLd4MVBlrVbfZo4wWgBslU4JZUaDikhFSH+GWMDgAq/rI32g==" + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -5085,6 +5421,70 @@ "node": ">=4" } }, + "node_modules/antd": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.20.0.tgz", + "integrity": "sha512-wWCFzbry3hov7k8gqhPR+FzD6EkWlhBbGD9mYOSIDoYRGMRqueTh2+2jfU1voHucmwcxDwzU7iwZDU2+PCXZdA==", + "dependencies": { + "@ant-design/colors": "^7.1.0", + "@ant-design/cssinjs": "^1.21.0", + "@ant-design/cssinjs-utils": "^1.0.3", + "@ant-design/icons": "^5.4.0", + "@ant-design/react-slick": "~1.1.2", + "@babel/runtime": "^7.24.8", + "@ctrl/tinycolor": "^3.6.1", + "@rc-component/color-picker": "~2.0.0", + "@rc-component/mutate-observer": "^1.1.0", + "@rc-component/qrcode": "~1.0.0", + "@rc-component/tour": "~1.15.0", + "@rc-component/trigger": "^2.2.0", + "classnames": "^2.5.1", + "copy-to-clipboard": "^3.3.3", + "dayjs": "^1.11.11", + "rc-cascader": "~3.27.0", + "rc-checkbox": "~3.3.0", + "rc-collapse": "~3.7.3", + "rc-dialog": "~9.5.2", + "rc-drawer": "~7.2.0", + "rc-dropdown": "~4.2.0", + "rc-field-form": "~2.2.1", + "rc-image": "~7.9.0", + "rc-input": "~1.6.2", + "rc-input-number": "~9.2.0", + "rc-mentions": "~2.15.0", + "rc-menu": "~9.14.1", + "rc-motion": "^2.9.2", + "rc-notification": "~5.6.0", + "rc-pagination": "~4.2.0", + "rc-picker": "~4.6.11", + "rc-progress": "~4.0.0", + "rc-rate": "~2.13.0", + "rc-resize-observer": "^1.4.0", + "rc-segmented": "~2.3.0", + "rc-select": "~14.15.1", + "rc-slider": "~11.1.3", + "rc-steps": "~6.0.1", + "rc-switch": "~4.1.0", + "rc-table": "~7.45.7", + "rc-tabs": "~15.1.1", + "rc-textarea": "~1.8.1", + "rc-tooltip": "~6.2.0", + "rc-tree": "~5.8.8", + "rc-tree-select": "~5.22.1", + "rc-upload": "~4.6.0", + "rc-util": "^5.43.0", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -5162,6 +5562,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-tree-filter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-tree-filter/-/array-tree-filter-2.1.0.tgz", + "integrity": "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==" + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -5384,6 +5789,29 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", @@ -6057,6 +6485,11 @@ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==" }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -6086,6 +6519,14 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6214,6 +6655,11 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz", + "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -6269,6 +6715,14 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/core-js": { "version": "3.37.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", @@ -6697,96 +7151,484 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==" + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } }, - "node_modules/data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", "dependencies": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" + "internmap": "1 - 2" }, "engines": { - "node": ">=10" + "node": ">=12" } }, - "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=12" } }, - "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "d3-path": "1 - 3" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=12" } }, - "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "d3-array": "^3.2.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=12" } }, - "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", "dependencies": { - "ms": "2.1.2" + "delaunator": "5" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=12" } }, - "node_modules/decimal.js": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } }, - "node_modules/dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==" + }, + "node_modules/dat.gui": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/dat.gui/-/dat.gui-0.7.9.tgz", + "integrity": "sha512-sCNc1OHobc+Erc1HqiswYgHdVNpSJUlk/Hz8vzOCsER7rl+oF/4+v8GXFUyCgtXpoCX6+bnmg07DedLvBLwYKQ==" + }, + "node_modules/data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "dependencies": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dayjs": { + "version": "1.11.12", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz", + "integrity": "sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==" + }, + "node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + }, + "node_modules/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==" }, "node_modules/deep-equal": { @@ -6884,6 +7726,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -7449,15 +8299,16 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -8805,6 +9656,24 @@ "node": ">=6.9.0" } }, + "node_modules/geotiff": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-2.1.3.tgz", + "integrity": "sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==", + "dependencies": { + "@petamoriken/float16": "^3.4.7", + "lerc": "^3.0.0", + "pako": "^2.0.4", + "parse-headers": "^2.0.2", + "quick-lru": "^6.1.1", + "web-worker": "^1.2.0", + "xml-utils": "^1.0.2", + "zstddec": "^0.1.0" + }, + "engines": { + "node": ">=10.19" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -9029,13 +9898,34 @@ "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==" }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/has-flag": { "version": "3.0.0", @@ -9501,6 +10391,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/ipaddr.js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", @@ -12175,6 +13073,14 @@ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "dependencies": { + "string-convert": "^0.2.0" + } + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -12298,6 +13204,11 @@ "shell-quote": "^1.8.1" } }, + "node_modules/lerc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz", + "integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -12368,6 +13279,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -12393,6 +13309,84 @@ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loglevel-colored-level-prefix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/loglevel-colored-level-prefix/-/loglevel-colored-level-prefix-1.0.0.tgz", + "integrity": "sha512-u45Wcxxc+SdAlh4yeF/uKlC1SPUPCy0gullSNKXod5I4bmifzk+Q4lSLExNEVn19tGaJipbZ4V4jbFn79/6mVA==", + "dev": true, + "dependencies": { + "chalk": "^1.1.3", + "loglevel": "^1.4.1" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -12420,6 +13414,14 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.456.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.456.0.tgz", + "integrity": "sha512-DIIGJqTT5X05sbAsQ+OhA8OtJYyD4NsEMCA/HQW/Y6ToPQ7gwbtujIoeAaup4HpHzV35SQOarKAWH8LYglB6eA==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -12466,6 +13468,11 @@ "tmpl": "1.0.5" } }, + "node_modules/material-colors": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" + }, "node_modules/mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", @@ -12779,6 +13786,14 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/numcodecs": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/numcodecs/-/numcodecs-0.2.2.tgz", + "integrity": "sha512-Y5K8mv80yb4MgVpcElBkUeMZqeE4TrovxRit/dTZvoRl6YkB6WEjY+fiUjGCblITnt3T3fmrDg8yRWu0gOLjhQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/nwsapi": { "version": "2.2.12", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", @@ -13058,6 +14073,11 @@ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -13078,6 +14098,11 @@ "node": ">=6" } }, + "node_modules/parse-headers": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", + "integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==" + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -14501,6 +15526,246 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-eslint": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-16.3.0.tgz", + "integrity": "sha512-Lh102TIFCr11PJKUMQ2kwNmxGhTsv/KzUg9QYF2Gkw259g/kPgndZDWavk7/ycbRvj2oz4BPZ1gCU8bhfZH/Xg==", + "dev": true, + "dependencies": { + "@typescript-eslint/parser": "^6.7.5", + "common-tags": "^1.4.0", + "dlv": "^1.1.0", + "eslint": "^8.7.0", + "indent-string": "^4.0.0", + "lodash.merge": "^4.6.0", + "loglevel-colored-level-prefix": "^1.0.0", + "prettier": "^3.0.1", + "pretty-format": "^29.7.0", + "require-relative": "^0.8.7", + "typescript": "^5.2.2", + "vue-eslint-parser": "^9.1.0" + }, + "engines": { + "node": ">=16.10.0" + }, + "peerDependencies": { + "prettier-plugin-svelte": "^3.0.0", + "svelte-eslint-parser": "*" + }, + "peerDependenciesMeta": { + "prettier-plugin-svelte": { + "optional": true + }, + "svelte-eslint-parser": { + "optional": true + } + } + }, + "node_modules/prettier-eslint/node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/prettier-eslint/node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/prettier-eslint/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prettier-eslint/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/prettier-eslint/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/prettier-eslint/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/prettier-eslint/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/prettier-eslint/node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -14602,125 +15867,727 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "engines": { - "node": ">= 0.10" + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", + "integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rc-cascader": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.27.0.tgz", + "integrity": "sha512-z5uq8VvQadFUBiuZJ7YF5UAUGNkZtdEtcEYiIA94N/Kc2MIKr6lEbN5HyVddvYSgwWlKqnL6pH5bFXFuIK3MNg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "array-tree-filter": "^2.1.0", + "classnames": "^2.3.1", + "rc-select": "~14.15.0", + "rc-tree": "~5.8.1", + "rc-util": "^5.37.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-checkbox": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.3.0.tgz", + "integrity": "sha512-Ih3ZaAcoAiFKJjifzwsGiT/f/quIkxJoklW4yKGho14Olulwn8gN7hOBve0/WGDg5o/l/5mL0w7ff7/YGvefVw==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.25.2" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-collapse": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.7.3.tgz", + "integrity": "sha512-60FJcdTRn0X5sELF18TANwtVi7FtModq649H11mYF1jh83DniMoM4MqY627sEKRCTm4+WXfGDcB7hY5oW6xhyw==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.3.4", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dialog": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.5.2.tgz", + "integrity": "sha512-qVUjc8JukG+j/pNaHVSRa2GO2/KbV2thm7yO4hepQ902eGdYK913sGkwg/fh9yhKYV1ql3BKIN2xnud3rEXAPw==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/portal": "^1.0.0-8", + "classnames": "^2.2.6", + "rc-motion": "^2.3.0", + "rc-util": "^5.21.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-drawer": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.2.0.tgz", + "integrity": "sha512-9lOQ7kBekEJRdEpScHvtmEtXnAsy+NGDXiRWc2ZVC7QXAazNVbeT4EraQKYwCME8BJLa8Bxqxvs5swwyOepRwg==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@rc-component/portal": "^1.1.1", + "classnames": "^2.2.6", + "rc-motion": "^2.6.1", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dropdown": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.0.tgz", + "integrity": "sha512-odM8Ove+gSh0zU27DUj5cG1gNKg7mLWBYzB5E4nNLrLwBmYEgYP43vHKDGOVZcJSVElQBI0+jTQgjnq0NfLjng==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-util": "^5.17.0" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/rc-field-form": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.2.1.tgz", + "integrity": "sha512-uoNqDoR7A4tn4QTSqoWPAzrR7ZwOK5I+vuZ/qdcHtbKx+ZjEsTg7QXm2wk/jalDiSksAQmATxL0T5LJkRREdIA==", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/async-validator": "^5.0.3", + "rc-util": "^5.32.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-image": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.9.0.tgz", + "integrity": "sha512-l4zqO5E0quuLMCtdKfBgj4Suv8tIS011F5k1zBBlK25iMjjiNHxA0VeTzGFtUZERSA45gvpXDg8/P6qNLjR25g==", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/portal": "^1.0.2", + "classnames": "^2.2.6", + "rc-dialog": "~9.5.2", + "rc-motion": "^2.6.2", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-input": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.6.2.tgz", + "integrity": "sha512-nJqsiIv8K88w8pvbUR5savKqBokdSR0zVGPntLApeOKFp8dp6s92l1CzD60yVActpCZAJwlCfRX5rno+QVYV7g==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.18.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-input-number": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.2.0.tgz", + "integrity": "sha512-5XZFhBCV5f9UQ62AZ2hFbEY8iZT/dm23Q1kAg0H8EvOgD3UDbYYJAayoVIkM3lQaCqYAW5gV0yV3vjw1XtzWHg==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/mini-decimal": "^1.0.1", + "classnames": "^2.2.5", + "rc-input": "~1.6.0", + "rc-util": "^5.40.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-mentions": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.15.0.tgz", + "integrity": "sha512-f5v5i7VdqvBDXbphoqcQWmXDif2Msd2arritVoWybrVDuHE6nQ7XCYsybHbV//WylooK52BFDouFvyaRDtXZEw==", + "dependencies": { + "@babel/runtime": "^7.22.5", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-input": "~1.6.0", + "rc-menu": "~9.14.0", + "rc-textarea": "~1.8.0", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-menu": { + "version": "9.14.1", + "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.14.1.tgz", + "integrity": "sha512-5wlRb3M8S4yGlWhSoEYJ7ZVRElyScdcpUHxgiLxkeig1tEdyKrnED3B2fhpN0Rrpdp9jyhnmZR/Lwq2fH5VvDQ==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.0.0", + "classnames": "2.x", + "rc-motion": "^2.4.3", + "rc-overflow": "^1.3.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-motion": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.2.tgz", + "integrity": "sha512-fUAhHKLDdkAXIDLH0GYwof3raS58dtNUmzLF2MeiR8o6n4thNpSDQhOqQzWE4WfFZDCi9VEN8n7tiB7czREcyw==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.43.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-notification": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.0.tgz", + "integrity": "sha512-TGQW5T7waOxLwgJG7fXcw8l7AQiFOjaZ7ISF5PrU526nunHRNcTMuzKihQHaF4E/h/KfOCDk3Mv8eqzbu2e28w==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.9.0", + "rc-util": "^5.20.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-overflow": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.3.2.tgz", + "integrity": "sha512-nsUm78jkYAoPygDAcGZeC2VwIg/IBGSodtOY3pMof4W3M9qRJgqaDYm03ZayHlde3I6ipliAxbN0RUcGf5KOzw==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.37.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-pagination": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-4.2.0.tgz", + "integrity": "sha512-V6qeANJsT6tmOcZ4XiUmj8JXjRLbkusuufpuoBw2GiAn94fIixYjFLmbruD1Sbhn8fPLDnWawPp4CN37zQorvw==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-picker": { + "version": "4.6.11", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.6.11.tgz", + "integrity": "sha512-PEVH5MMTUrdvTTxCmPndsXiJL7TFLSu8q0cDdZrhdcjn8en3NbuhOFacWqKTvdnfG53RPPhiBssXCUHYyc3R/Q==", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.1", + "rc-overflow": "^1.3.2", + "rc-resize-observer": "^1.4.0", + "rc-util": "^5.43.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/rc-progress": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz", + "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.6", + "rc-util": "^5.16.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-rate": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.0.tgz", + "integrity": "sha512-oxvx1Q5k5wD30sjN5tqAyWTvJfLNNJn7Oq3IeS4HxWfAiC4BOXMITNAsw7u/fzdtO4MS8Ki8uRLOzcnEuoQiAw==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.0.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-resize-observer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.0.tgz", + "integrity": "sha512-PnMVyRid9JLxFavTjeDXEXo65HCRqbmLBw9xX9gfC4BZiSzbLXKzW3jPz+J0P71pLbD5tBMTT+mkstV5gD0c9Q==", + "dependencies": { + "@babel/runtime": "^7.20.7", + "classnames": "^2.2.1", + "rc-util": "^5.38.0", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-segmented": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.3.0.tgz", + "integrity": "sha512-I3FtM5Smua/ESXutFfb8gJ8ZPcvFR+qUgeeGFQHBOvRiRKyAk4aBE5nfqrxXx+h8/vn60DQjOt6i4RNtrbOobg==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-motion": "^2.4.4", + "rc-util": "^5.17.0" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-select": { + "version": "14.15.1", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.15.1.tgz", + "integrity": "sha512-mGvuwW1RMm1NCSI8ZUoRoLRK51R2Nb+QJnmiAvbDRcjh2//ulCkxeV6ZRFTECPpE1t2DPfyqZMPw90SVJzQ7wQ==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.1.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-overflow": "^1.3.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-slider": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.5.tgz", + "integrity": "sha512-b77H5PbjMKsvkYXAYIkn50QuFX6ICQmCTibDinI9q+BHx65/TV4TeU25+oadhSRzykxs0/vBWeKBwRyySOeWlg==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-steps": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz", + "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==", + "dependencies": { + "@babel/runtime": "^7.16.7", + "classnames": "^2.2.3", + "rc-util": "^5.16.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" + "node_modules/rc-switch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz", + "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==", + "dependencies": { + "@babel/runtime": "^7.21.0", + "classnames": "^2.2.1", + "rc-util": "^5.30.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", - "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "node_modules/rc-table": { + "version": "7.45.7", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.45.7.tgz", + "integrity": "sha512-wi9LetBL1t1csxyGkMB2p3mCiMt+NDexMlPbXHvQFmBBAsMxrgNSAPwUci2zDLUq9m8QdWc1Nh8suvrpy9mXrg==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/context": "^1.4.0", + "classnames": "^2.2.5", + "rc-resize-observer": "^1.1.0", + "rc-util": "^5.37.0", + "rc-virtual-list": "^3.14.2" + }, "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "node_modules/rc-tabs": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.1.1.tgz", + "integrity": "sha512-Tc7bJvpEdkWIVCUL7yQrMNBJY3j44NcyWS48jF/UKMXuUlzaXK+Z/pEL5LjGcTadtPvVmNqA40yv7hmr+tCOAw==", "dependencies": { - "side-channel": "^1.0.4" + "@babel/runtime": "^7.11.2", + "classnames": "2.x", + "rc-dropdown": "~4.2.0", + "rc-menu": "~9.14.0", + "rc-motion": "^2.6.2", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.34.1" }, "engines": { - "node": ">=0.6" + "node": ">=8.x" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/raf": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "node_modules/rc-textarea": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.8.1.tgz", + "integrity": "sha512-bm36N2ZqwZAP60ZQg2OY9mPdqWC+m6UTjHc+CqEZOxb3Ia29BGHazY/s5bI8M4113CkqTzhtFUDNA078ZiOx3Q==", "dependencies": { - "performance-now": "^2.1.0" + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.1", + "rc-input": "~1.6.0", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "node_modules/rc-tooltip": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.2.0.tgz", + "integrity": "sha512-iS/3iOAvtDh9GIx1ulY7EFUXUtktFccNLsARo3NPgLf0QW9oT0w3dA9cYWlhqAKmD+uriEwdWz1kH0Qs4zk2Aw==", "dependencies": { - "safe-buffer": "^5.1.0" + "@babel/runtime": "^7.11.2", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "node_modules/rc-tree": { + "version": "5.8.8", + "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.8.8.tgz", + "integrity": "sha512-S+mCMWo91m5AJqjz3PdzKilGgbFm7fFJRFiTDOcoRbD7UfMOPnerXwMworiga0O2XIo383UoWuEfeHs1WOltag==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.1" + }, "engines": { - "node": ">= 0.6" + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" } }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "node_modules/rc-tree-select": { + "version": "5.22.1", + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.22.1.tgz", + "integrity": "sha512-b8mAK52xEpRgS+b2PTapCt29GoIrO5cO8jB7AfHttFsIJfcnynY9FCtnYzURsKXJkGHbFY6UzSEB2I3TETtdWg==", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-select": "~14.15.0", + "rc-tree": "~5.8.1", + "rc-util": "^5.16.1" }, - "engines": { - "node": ">= 0.8" + "peerDependencies": { + "react": "*", + "react-dom": "*" } }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" + "node_modules/rc-upload": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.6.0.tgz", + "integrity": "sha512-Zr0DT1NHw/ApxrP7UAoxOtGaVYuzarrrCVr0ld7RiEFsKX07uFhE1EpCBxwL11ruFn89GMcshOKWp+s6FLyAlA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "classnames": "^2.2.5", + "rc-util": "^5.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "node_modules/rc-util": { + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.43.0.tgz", + "integrity": "sha512-AzC7KKOXFqAdIBqdGWepL9Xn7cm3vnAmjlHqUnoQaTMZYhM4VlXGLkkHHxj/BZ7Td0+SOPKB4RGPboBVKT9htw==", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "node_modules/rc-virtual-list": { + "version": "3.14.5", + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.14.5.tgz", + "integrity": "sha512-ZMOnkCLv2wUN8Jz7yI4XiSLa9THlYvf00LuMhb1JlsQCewuU7ydPuHw1rGVPhe9VZYl/5UqODtNd7QKJ2DMGfg==", + "dependencies": { + "@babel/runtime": "^7.20.0", + "classnames": "^2.2.6", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.36.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/re-resizable": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.10.0.tgz", + "integrity": "sha512-hysSK0xmA5nz24HBVztlk4yCqCLCvS32E6ZpWxVKop9x3tqCa4yAj1++facrmkOf62JsJHjmjABdKxXofYioCw==", + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0" } }, "node_modules/react": { @@ -14755,6 +16622,23 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, + "node_modules/react-color": { + "version": "2.19.3", + "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", + "integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==", + "dependencies": { + "@icons/material": "^0.2.4", + "lodash": "^4.17.15", + "lodash-es": "^4.17.15", + "material-colors": "^1.2.1", + "prop-types": "^15.5.10", + "reactcss": "^1.2.0", + "tinycolor2": "^1.4.1" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -14941,6 +16825,19 @@ "react": "^18.3.1" } }, + "node_modules/react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, "node_modules/react-error-overlay": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", @@ -14959,6 +16856,55 @@ "node": ">=0.10.0" } }, + "node_modules/react-rnd": { + "version": "10.4.13", + "resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.4.13.tgz", + "integrity": "sha512-Vgbf0iihspcQ6nkaFhpOGWfmnuVbhkhoB0hBbYl8aRDA4horsQHESc4E1z7O/P27kFFjK2aqM0u5CGzfr9gEZA==", + "dependencies": { + "re-resizable": "6.10.0", + "react-draggable": "4.4.6", + "tslib": "2.6.2" + }, + "peerDependencies": { + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, + "node_modules/react-rnd/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/react-router": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz", + "integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==", + "dependencies": { + "@remix-run/router": "1.20.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz", + "integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==", + "dependencies": { + "@remix-run/router": "1.20.0", + "react-router": "6.27.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -15031,6 +16977,14 @@ } } }, + "node_modules/reactcss": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", + "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", + "dependencies": { + "lodash": "^4.0.1" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -15086,6 +17040,11 @@ "node": ">=8" } }, + "node_modules/reference-spec-reader": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/reference-spec-reader/-/reference-spec-reader-0.2.0.tgz", + "integrity": "sha512-q0mfCi5yZSSHXpCyxjgQeaORq3tvDsxDyzaadA/5+AbAUwRyRuuTh0aRQuE/vAOt/qzzxidJ5iDeu1cLHaNBlQ==" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -15228,11 +17187,22 @@ "node": ">=0.10.0" } }, + "node_modules/require-relative": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", + "integrity": "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==", + "dev": true + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -15369,6 +17339,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "node_modules/rollup": { "version": "2.79.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", @@ -15460,6 +17435,11 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, "node_modules/safe-array-concat": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", @@ -15632,6 +17612,14 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -15701,6 +17689,31 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/serialize-error": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.3.tgz", + "integrity": "sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==", + "dependencies": { + "type-fest": "^2.12.2" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -16147,6 +18160,11 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==" + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -16402,6 +18420,11 @@ "postcss": "^8.2.15" } }, + "node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -16819,16 +18842,34 @@ "node": ">=0.8" } }, + "node_modules/three": { + "version": "0.167.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.167.1.tgz", + "integrity": "sha512-gYTLJA/UQip6J/tJvl91YYqlZF47+D/kxiWrbTon35ZHlXEN0VOo+Qke2walF1/x92v55H6enomymg4Dak52kw==" + }, "node_modules/throat": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz", "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==" }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "engines": { + "node": ">=12.22" + } + }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -16853,6 +18894,11 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -16899,6 +18945,18 @@ "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" }, + "node_modules/ts-api-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.0.tgz", + "integrity": "sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -16958,6 +19016,14 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/tweakpane": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/tweakpane/-/tweakpane-3.1.10.tgz", + "integrity": "sha512-rqwnl/pUa7+inhI2E9ayGTqqP0EPOOn/wVvSWjZsRbZUItzNShny7pzwL3hVlaN4m9t/aZhsP0aFQ9U5VVR2VQ==", + "funding": { + "url": "https://github.com/sponsors/cocopon" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -17182,6 +19248,17 @@ "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==" }, + "node_modules/unzipit": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unzipit/-/unzipit-1.4.3.tgz", + "integrity": "sha512-gsq2PdJIWWGhx5kcdWStvNWit9FVdTewm4SEG7gFskWs+XCVaULt9+BwuoBtJiRE8eo3L1IPAOrbByNLtLtIlg==", + "dependencies": { + "uzip-module": "^1.0.2" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/upath": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", @@ -17277,6 +19354,11 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/uzip-module": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/uzip-module/-/uzip-module-1.0.3.tgz", + "integrity": "sha512-AMqwWZaknLM77G+VPYNZLEruMGWGzyigPK3/Whg99B3S6vGHuqsyl5ZrOv1UUF3paGK1U6PM0cnayioaryg/fA==" + }, "node_modules/v8-to-istanbul": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", @@ -17303,6 +19385,30 @@ "node": ">= 0.8" } }, + "node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -17356,6 +19462,11 @@ "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz", "integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==" }, + "node_modules/web-worker": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz", + "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==" + }, "node_modules/webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", @@ -18180,6 +20291,11 @@ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" }, + "node_modules/xml-utils": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.1.tgz", + "integrity": "sha512-Dn6vJ1Z9v1tepSjvnCpwk5QqwIPcEFKdgnjqfYOABv1ngSofuAhtlugcUC3ehS1OHdgDWSG6C5mvj+Qm15udTQ==" + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", @@ -18231,6 +20347,19 @@ "node": ">=10" } }, + "node_modules/yarn": { + "version": "1.22.22", + "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.22.tgz", + "integrity": "sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg==", + "hasInstallScript": true, + "bin": { + "yarn": "bin/yarn.js", + "yarnpkg": "bin/yarn.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -18241,6 +20370,21 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zarrita": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/zarrita/-/zarrita-0.3.2.tgz", + "integrity": "sha512-Zx9nS28C2tXZhF1BmQkgQGi0M/Z5JiM/KCMa+fEYtr/MnIzyizR4sKRA/sXjDP1iuylILWTJAWWBJD//0ONXCA==", + "dependencies": { + "@zarrita/core": "^0.0.3", + "@zarrita/indexing": "^0.0.3", + "@zarrita/storage": "^0.0.2" + } + }, + "node_modules/zstddec": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.1.0.tgz", + "integrity": "sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==" } } } diff --git a/package.json b/package.json index 249a261..73cc3c1 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,24 @@ "version": "0.1.0", "private": true, "dependencies": { + "@aics/volume-viewer": "^3.11.2", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "antd": "^5.20.0", + "axios": "^1.7.7", + "d3": "^7.9.0", + "dat.gui": "^0.7.9", + "lucide-react": "^0.456.0", "react": "^18.3.1", + "react-color": "^2.19.3", "react-dom": "^18.3.1", + "react-rnd": "^10.4.13", + "react-router-dom": "^6.27.0", "react-scripts": "5.0.1", - "web-vitals": "^2.1.4" + "three": "^0.167.1", + "web-vitals": "^2.1.4", + "yarn": "^1.22.22" }, "scripts": { "start": "react-scripts start", @@ -34,5 +45,10 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "eslint": "^8.57.1", + "prettier": "^3.3.3", + "prettier-eslint": "^16.3.0" } } diff --git a/public/favicon.ico b/public/favicon.ico old mode 100644 new mode 100755 index a11777c..8550453 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html index aa069f2..0b9a6c5 100644 --- a/public/index.html +++ b/public/index.html @@ -24,7 +24,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - React App + ISAS LSFM diff --git a/public/logo192.png b/public/logo192.png index fc44b0a..443b4b5 100644 Binary files a/public/logo192.png and b/public/logo192.png differ diff --git a/public/logo512.png b/public/logo512.png index a4e47a6..7d7eb4e 100644 Binary files a/public/logo512.png and b/public/logo512.png differ diff --git a/src/App.css b/src/App.css index 74b5e05..100d945 100644 --- a/src/App.css +++ b/src/App.css @@ -1,38 +1,1548 @@ +:root { + --color-primary: #000000; /* Black for primary */ + --color-primary-dark: #333333; /* Dark grey as a substitute for dark primary */ + --color-secondary: #ffffff; /* White for secondary */ + --color-border: #e0e0e0; /* You might keep this for borders */ + --color-background: #f5f5f5; /* Light grey for backgrounds, if suitable */ + --color-tooltip: #000000; /* Black for tooltips */ + --color-tooltip-text: #ffffff; /* White for tooltip text */ +} .App { + display: flex; + flex-direction: column; + min-height: 100vh; /* Make sure the app fills the viewport height */ + padding-top: 60px; /* Adjusted for the fixed header */ + box-sizing: border-box; +} + +.content-wrap { + flex: 1; /* Allows this container to grow and fill available space, pushing the footer down */ + display: flex; + flex-direction: column; + margin-bottom: -60px; /* Adjust this value based on your footer's height */ +} + +/* Adjust the footer CSS */ +.app-footer { + background-color: var( + --color-primary + ); /* Using your primary color variable */ + color: var(--color-secondary); + padding: 20px; /* Adjust the padding as needed */ text-align: center; + width: 100%; + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border-top: 1px solid var(--color-border); /* Optional border */ + margin-top: auto; +} + +body { + font-family: "Roboto", sans-serif; +} +body, +html { + margin: auto; + padding: 0; + max-width: 100vw; + overflow-x: hidden; /* Prevent horizontal scroll */ +} + +.container { + display: flex; + flex-direction: row; + align-items: flex-start; + width: 95%; /* Adjust the percentage as needed */ + margin: 40px auto 40px 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.main-content { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; + position: relative; + min-width: 98%; + width: 100%; +} + +.content-container { + flex: 1; /* Each container occupies 50% of the horizontal space */ + margin-right: 20px; /* Add some spacing between the containers */ + overflow: hidden; /* Prevent content from overflowing */ +} + +.rounds-container { + display: grid; + grid-template-columns: repeat(4, calc(25% - 10px)); + gap: 4px; + /* margin-bottom: 20px; */ + /* margin-left: -153px; */ + /* margin-top: -97px; */ + z-index: 50; + position: relative; + top: 3px; + flex-grow: 1; +} + +.grid-with-indicators { + display: grid; + padding-top: 21px; + position: relative; + width: auto; + gap: 5px; +} + +.grid-with-indicators .horizontal-indicators { + grid-column: 2; + grid-row: 1; + margin-left: -2px; +} + +.grid-with-indicators .vertical-indicators { + grid-column: 1; + grid-row: 2; +} + +.grid-with-indicators .grid-container { + grid-column: 2; + grid-row: 2; +} + +.grid-with-indicators:hover { + transform: scale(1.5); /* Adjust the scale value as needed */ + z-index: 100; /* Ensure the zoomed element is above others */ + transition: transform 0.3s ease; /* Smooth zoom effect */ + /* position: absolute; Use absolute positioning */ +} + +.grid-container { + display: grid; + grid-template-columns: repeat( + 8, + 15px + ); /* Keeps the 15px width for each cell */ + grid-template-rows: repeat( + 8, + 15px + ); /* Ensures rows are also defined for clarity */ + border: 1px solid #e0e0e0; /* Maintains the border */ + background-color: #f5f5f5; /* Keeps the background color */ + border-radius: 5px; /* Maintains the border-radius */ + width: 120px; /* Adjusted width: 8 cells * 15px each */ + height: 120px; /* Adjusted height: 8 cells * 15px each */ + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); /* Keeps the shadow */ +} + +.grid-cell { + background-color: #adacaa; + border: 0.25px inherit; + border-color: #9e9c9d; + box-sizing: border-box; + cursor: pointer; + height: 15px; + position: relative; + transition: + box-shadow 0.3s ease, + transform 0.3s ease; + width: 15px; +} + +.tooltip { + visibility: hidden; + position: absolute; + background-color: var(--color-tooltip); + color: var(--color-tooltip-text); + padding: 8px 10px; + border-radius: 5px; + border: 1px solid #fff; + z-index: 1010 !important; + font-size: 0.7em; + width: 130px; + text-align: left; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2); + transition: + visibility 0.2s ease, + opacity 0.2s ease; + opacity: 0; /* Start with an invisible tooltip */ + pointer-events: none; /* Prevents the tooltip from blocking mouse events */ +} + +.grid-cell:hover .tooltip { + visibility: visible; + opacity: 1; /* Make the tooltip visible on hover */ + transition-delay: 0.5s; /* Add a delay to the transition */ +} + +.grid-cell:hover { + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); /* More pronounced shadow on hover */ + transform: scale(1.05); + z-index: 1; +} + +.grid-cell.selected { + position: relative; /* This ensures the pseudo-element is positioned relative to this cell */ + overflow: hidden; /* Prevents the pseudo-element from spilling outside the cell */ +} + +.grid-cell.selected::after { + content: ""; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-image: repeating-linear-gradient( + -45deg, + rgba(0, 0, 0, 0.2), + rgba(0, 0, 0, 0.2) 10px, + transparent 10px, + transparent 20px /* The size of the stripes */ + ); + z-index: 1; +} + +.tiff-player img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + /* object-fit: cover; This ensures the image covers the available space, similar to your video setup */ +} + +.player-controls { + position: absolute; + bottom: 0; + left: 0; + right: 0; + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + background-color: rgba( + 0, + 0, + 0, + 0.5 + ); /* Semi-transparent background for visibility */ +} + +.player-controls button { + cursor: pointer; + padding: 5px 10px; + background-color: var( + --color-primary + ); /* Use primary color for button background */ + color: var(--color-secondary); /* Use secondary color for text */ + border: none; + border-radius: 5px; + outline: none; + transition: background-color 0.3s ease; +} + +.player-controls button:hover { + background-color: var( + --color-primary-dark + ); /* Darker shade for hover state */ +} + +.progress-bar { + flex-grow: 1; + height: 20px; + background-color: #e9e9e9; + border-radius: 10px; + margin-left: 10px; + position: relative; + overflow: hidden; + background-color: var(--color-border); + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2); +} + +.progress { + height: 100%; + border-radius: 10px; + background-color: var( + --color-primary + ); /* Use primary color for progress bar */ + transition: width 0.3s ease; +} + +.scrubber { + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 12px; + height: 12px; + background-color: var( + --color-secondary + ); /* Use secondary color for scrubber */ + border: 2px solid var(--color-primary); /* Border color from primary */ + border-radius: 50%; + cursor: pointer; + z-index: 2; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4); +} + +.tiff-player { + position: relative; + width: 100%; + padding-top: 56.25%; /* Adjust this value to match the aspect ratio of your TIFF images */ + top: 76px; + flex-grow: 2; /* This ensures that the player takes up the space it needs, similar to your mp4 player setup */ } -.App-logo { - height: 40vmin; +.overlay { + position: absolute; /* Positions .overlay in relation to .tiff-player */ + top: 0; + left: 0; + right: 0; + bottom: 0; /* These four properties ensure .overlay matches the size of .tiff-player */ + display: flex; + justify-content: center; + align-items: center; + background: rgba(0, 0, 0, 0.1); + z-index: 1; /* Ensures .overlay stacks above the img */ + cursor: pointer; pointer-events: none; } -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } +.loading-bar { + width: 100%; + height: 4px; + background-color: #e0e0e0; + position: fixed; + top: 0; + left: 0; + z-index: 10; +} + +.loading-progress { + height: 100%; + width: 0; + background-color: #007bff; + transition: width 0.3s ease; +} + +.grid-cell.selected { + border: 2px solid #007bff; } -.App-header { - background-color: #282c34; - min-height: 100vh; +.round-header { + position: absolute; + top: 0; + left: 57%; + transform: translateX(-50%); + background-color: var(--color-secondary); /* light orange background */ + font-weight: bold; + z-index: 1; + padding: 2px 10px; + border-radius: 2px; /* rounding the corners */ + box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2); /* optional shadow for a lifted effect */ +} + +button { + margin: 10px; + padding: 10px 20px; + display: inline-block; + text-align: center; + vertical-align: middle; + cursor: pointer; + background-color: #007bff; + color: #fff; + border: none; + border-radius: 5px; + transition: background-color 0.3s; + outline: none; + font-size: 0.9em; + line-height: 1; + width: 120px; + height: 40px; + background-color: var(--color-primary); + color: var(--color-tooltip-text); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); /* Adds subtle shadow for depth */ +} + +button:hover { + background-color: var(--color-primary-dark); +} + +button:disabled { + background-color: #e0e0e0; + cursor: not-allowed; +} + +.grid-cell:not(.selected):hover { + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + transform: scale(1.05); + z-index: 1; +} + +.grid-container:hover { + border-color: #007bff; +} + +.horizontal-indicators, +.vertical-indicators { + display: flex; + justify-content: space-between; + align-items: center; + background-color: #f9f9f9; + padding: 5px; + font-size: 0.7em; /* Smaller font size */ +} + +.horizontal-indicators { + position: relative; + z-index: 1; + width: 100%; + height: auto; + flex-direction: row; + padding: 5px 5px 5px 0; +} + +.vertical-indicators { + height: 100%; + width: 0px; + flex-direction: column; + padding: 0px; +} + +.indicator-item { + text-align: center; + font-size: 0.8em; + font-weight: bold; + margin: 0 5px; +} + +.rounds-header-container { display: flex; flex-direction: column; align-items: center; + margin-bottom: 20px; /* Adding some spacing between the round headers and the rest of the content */ +} + +.rounds-header-group { + display: flex; + justify-content: space-between; /* Spread the round headers evenly */ + width: 100%; /* Take the full width of the parent container */ + background-color: #ffa500; /* Orange background */ + padding: 5px; /* Add some padding around the group */ + margin: 5px 0; /* Add some margin between the groups */ + border-radius: 5px; /* Add rounded corners */ + box-shadow: 0px 2px 15px rgba(0, 0, 0, 0.1); /* Add a subtle shadow for depth */ +} + +.color-legend { + display: flex; + flex-direction: column; /* Align legend items vertically */ + padding-right: 10px; /* Space between legend and histogram */ + padding-top: 49px; +} + +.gradient-box { + height: 200px; /* Match the height of your histogram */ + width: 20px; /* Width of the color bar */ + margin-bottom: 10px; /* Space between the gradient and labels */ +} + +.legend-marks { + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.legend-mark { + position: absolute; + left: -1px; + transform: translate(0, -1468%); + font-size: 0.8em; +} + +.text-center { + text-align: center; +} + +.side-panel { + background-color: #fff; /* White background */ + margin-top: -62px; + border-radius: 4px; /* Rounded corners */ + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* Shadow for depth */ + display: flex; + flex-direction: row; + overflow: hidden; + width: 90%; +} + +.side-panel .panel-header { + font-size: 1.25rem; + background-color: var(--color-primary); /* Black background */ + color: var(--color-secondary); /* White text */ + text-align: center; + font-weight: 500; + padding: 8px 0; /* Padding for the header */ + border-bottom: 1px solid var(--color-border); + width: 100%; +} + +.side-panel .panel-content { + padding: 16px; + flex-grow: 1; /* Allows this element to fill up the remaining space */ + margin-left: -32px; + overflow-y: auto; /* Allows scrolling if content is too long */ +} + +.side-panel .panel-item { + padding: 12px 16px; + border-bottom: 1px solid var(--color-border); + transition: background-color 0.2s; + cursor: pointer; +} + +.side-panel .panel-item:hover { + background-color: var(--color-background); +} + +.histogram { + margin-top: 20px; /* Space above the histogram */ +} + +.histogram-bar { + display: flex; + align-items: center; + margin-bottom: 5px; +} + +.histogram-bar-fill { + height: 20px; + background-color: var(--color-primary); /* Black bars for the histogram */ + margin-right: 10px; + transition: width 3s ease-in-out; +} + +.histogram-bar-label { + font-size: 0.8em; + color: var(--color-primary-dark); /* Dark grey text */ +} + +/* For screens wider than 1920px */ +@media (min-width: 1920px) { + .container { + width: 90%; /* You can go up to 100% if you want to use all the horizontal space */ + max-width: none; /* This will allow the container to adjust to the percentage width */ + } + + .main-content { + flex: 0 0 100%; /* Adjust the flex-basis as needed */ + } +} + +/* For screens wider than 3840px */ +@media (min-width: 3840px) { + .container { + width: 80%; /* Less percentage as the screen is very wide */ + } +} + +.navbar { + background-color: black; /* As per your color scheme */ + color: white; + padding: 10px 20px; + display: flex; + align-items: center; + justify-content: space-between; /* This spreads out the logo and title */ +} + +.navbar h1 { + margin-top: 0; /* Removes any top margin from the h1 for accurate centering */ + flex-grow: 1; /* Allows the h1 to take up the remaining space */ + text-align: center; /* Centers the title in the available space */ +} + +.logo { + height: 50px; + margin-right: 10px; /* Adds a little space between logo and title */ +} + +.NotFound { + display: flex; + align-items: center; justify-content: center; - font-size: calc(10px + 2vmin); + position: relative; + height: 100vh; color: white; + background-color: black; + text-align: center; + overflow: hidden; +} + +.solar-system { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 50%; + transform: translateX(-50%); + z-index: 1; +} + +.sun { + position: absolute; + top: 50%; + left: 50%; + width: 100px; + height: 100px; + background-color: yellow; + border-radius: 50%; + transform: translate(-50%, -50%); + box-shadow: 0 0 100px yellow; /* Glowing effect */ +} + +.orbit { + position: absolute; + top: 50%; + left: 50%; + width: 300px; + height: 300px; + border: 1px dashed white; + border-radius: 50%; + transform: translate(-50%, -50%); } -.App-link { - color: #61dafb; +.planet { + position: absolute; + top: 0; + left: 0; + width: 20px; + height: 20px; + background-color: blue; + border-radius: 50%; + animation: orbitAnimation 10s linear infinite; } -@keyframes App-logo-spin { +@keyframes orbitAnimation { from { - transform: rotate(0deg); + transform: rotate(0deg) translateX(150px); } to { + transform: rotate(360deg) translateX(150px); + } +} + +.NotFound-content { + position: relative; + z-index: 2; +} + +.NotFound-title { + font-size: 5rem; + margin: 0; +} + +.NotFound-message { + font-size: 1.5rem; +} + +.NotFound-link { + display: inline-block; + margin-top: 20px; + padding: 10px 20px; + background-color: #ff4500; + color: white; + border: none; + border-radius: 4px; + text-decoration: none; + transition: background-color 0.3s; +} + +.NotFound-link:hover { + background-color: #ff5733; +} + +.histogram-tooltip { + position: absolute; + visibility: hidden; + background-color: var(--color-tooltip); + color: var(--color-tooltip-text); + text-align: center; + border-radius: 4px; + padding: 5px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + z-index: 2; + white-space: nowrap; + pointer-events: none; + transition: + visibility 0.2s ease, + opacity 0.2s ease; + opacity: 0; + top: -35px; /* Adjust the position above the hovered element */ + left: 50%; + transform: translateX(-50%); /* Center the tooltip */ +} + +.histogram-bar:hover .histogram-tooltip { + visibility: visible; + opacity: 1; +} + +.histogram-bar { + position: relative; /* To position the tooltip */ +} + +.zoom-controls { + margin-top: 20px; + display: flex; + flex-direction: column; + align-items: center; +} + +.zoom-controls button { + margin-bottom: 10px; +} + +.tooltip.top-left { + bottom: 100%; + left: 0; + transform: translateY(-10px); /* Adjust as needed */ +} + +.tooltip.top-right { + bottom: 100%; + right: 0; + transform: translateY(-10px); /* Adjust as needed */ +} + +.tiff-player-placeholder, +.loading-overlay { + position: relative; /* Needed for absolute positioning of the overlay */ + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 684px; + width: 100%; + background-color: #f5f5f5; + border: 1px solid #d3d3d3; + border-radius: 5px; + margin-top: 50px; + /* margin-left: -104px; */ + color: #333; + text-align: center; + padding: 20px; +} + +.loading-overlay { + /* position: absolute; */ + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + background-color: var( + --color-background + ); /* Semi-transparent white background */ + z-index: 10; /* Ensures it's above other elements */ +} + +.loading-circle { + border: 5px solid #f3f3f3; /* Light grey border */ + border-top: 5px solid #3498db; /* Blue border */ + border-radius: 50%; + width: 50px; + height: 50px; + animation: spin 2s linear infinite; +} + +.tiff-player-placeholder p { + margin-top: 15px; + font-size: 1.1em; /* Slightly larger font size */ + color: #333; /* Darker text for better readability */ +} + +/* Define a specific size for the chart container */ +.histogram-chart-container { + width: 100%; /* Use the full width of the side panel */ + height: 300px; /* Set a fixed height for the chart */ +} + +.histogram-and-legend-container { + display: flex; + align-items: stretch; /* Align items vertically in the center */ + flex-direction: column; +} + +.histogram-and-legend-container-contents { + display: flex; + align-items: stretch; /* Align items vertically in the center */ +} + +.loading-circle { + border: 5px solid #f3f3f3; /* Light grey border */ + border-top: 5px solid #3498db; /* Blue border */ + border-radius: 50%; + width: 60px; /* Same size as FaPlay icon */ + height: 60px; /* Same size as FaPlay icon */ + animation: spin 2s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { transform: rotate(360deg); } } + +.jumbotron { + background-color: #f9f9f9; /* Light grey background */ + padding: 20px; + margin-top: 43px; /* Small top margin to create spacing */ + border-radius: 5px; /* Rounded corners */ + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); /* Shadow for depth */ + text-align: center; /* Center the text */ + height: 150px; /* Fixed height to prevent resizing */ + overflow: hidden; /* Hide overflowing content */ +} + +.jumbotron h2 { + margin-top: 0; + color: var(--color-primary); /* Black color for the heading */ +} + +.jumbotron p { + color: var(--color-primary-dark); /* Dark grey for the paragraph */ +} + +button { + display: inline-flex; /* Use flex to align items horizontally */ + align-items: center; /* Center items vertically within the button */ + justify-content: center; /* Center items horizontally within the button */ + margin: 10px; + padding: 10px 15px; /* Adjust padding to give more space */ + text-align: center; + vertical-align: middle; + cursor: pointer; + background-color: #007bff; + color: #fff; + border: none; + border-radius: 5px; + transition: background-color 0.3s; + outline: none; + font-size: 0.9em; + line-height: 1; + min-width: 122px; /* Set a minimum width to accommodate text and icon */ + height: 46px; + background-color: var(--color-primary); + color: var(--color-tooltip-text); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +button:hover { + background-color: var(--color-primary-dark); +} + +button:disabled { + background-color: #e0e0e0; + cursor: not-allowed; +} + +/* Adjust icon size and margin */ +button svg { + width: 20px; /* Adjust the icon size */ + height: 20px; /* Adjust the icon size */ + margin-right: 5px; /* Add some space between icon and text */ +} + +.selected-tiff-tags { + display: flex; + flex-wrap: wrap; + margin-top: 10px; +} + +.tiff-tag { + background-color: #007bff; + color: white; + padding: 5px 10px; + margin-right: 5px; + margin-bottom: 5px; + border-radius: 15px; + font-size: 0.85em; + white-space: nowrap; +} + +.color-legend { + display: flex; + flex-direction: column; + align-items: center; +} + +.gradient-box { + width: 30px; /* Adjust as needed */ + height: 200px; /* Adjust as needed */ + margin: 10px 0; +} + +.legend-labels .legend-label { + position: absolute; + left: 35px; /* Adjust as needed */ + /* Other styling as needed */ +} + +.mp4-player { + position: relative; + width: 100%; + padding-top: 50.25%; + top: 75px; + flex-grow: 2; + min-width: 320px; /* Base min-width for small devices */ +} + +.mp4-player video { + position: absolute; + top: -25px; + left: 0; + width: 100%; + max-height: 95%; + object-fit: cover; +} + +/* Adjust for tablets */ +@media (max-width: 768px) { + .container { + width: 100%; /* Use more of the screen width on smaller devices */ + margin: 20px auto; /* Adjust margin to fit smaller screens */ + } +} + +/* Adjust for larger screens */ +@media (min-width: 1024px) { + .container { + width: 90%; /* Adjust width as needed */ + margin: 40px auto; /* Center with appropriate margin */ + } +} + +.polar-plot-display { + background-color: var(--color-secondary); /* White background */ + margin: 20px auto; /* Center the widget and add vertical space */ + padding: 15px; /* Padding inside the container */ + border-radius: 8px; /* Rounded corners */ + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* Subtle shadow for depth */ + max-width: 95%; /* Limit the maximum width */ + text-align: center; /* Center align the content */ +} + +.polar-plot-display img { + max-width: 100%; /* Ensure the image is responsive */ + height: auto; /* Maintain aspect ratio */ + border-radius: 5px; /* Slightly rounded corners for the image */ + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); /* Soft shadow around the image */ +} + +.polar-plot-display h2 { + margin-bottom: 15px; /* Space between title and image */ + color: var(--color-primary); /* Black color for the text */ + font-size: 1.5rem; /* Larger font size for the title */ +} +.polar-plot-container { + text-align: center; + padding: 10px; + margin-top: 20px; + transition: transform 0.3s ease-in-out; + display: flex; + flex-direction: column; /* This will stack children vertically */ + align-items: center; /* Center align items for good measure */ +} + +.polar-plot-image { + max-width: 95%; + height: auto; + border-radius: 10px; /* Rounded corners for the image */ + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); /* Shadow for depth */ + transition: all 0.3s ease-in-out; +} + +.polar-plot-container:hover .polar-plot-image { + transform: scale(1.05); /* Slightly enlarge the image on hover */ + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3); /* More pronounced shadow on hover */ +} + +/* Animation for the loading of the polar plot */ +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.polar-plot-image { + animation: fadeIn 1s; /* Apply the animation to the image */ +} + +.typewriter-effect { + white-space: pre-wrap; /* Allows for line breaks in your text */ + font-family: monospace; /* Gives the typewriter effect more authenticity */ +} + +.cursor { + display: inline-block; + width: 2px; + height: 1em; + background-color: currentColor; + margin-left: 2px; + animation: blink 1s infinite; +} + +@keyframes blink { + 0%, + 50% { + opacity: 1; + } + 50.01%, + 100% { + opacity: 0; + } +} + +.typewriter-link { + color: #007bff; + text-decoration: none; +} + +.typewriter-link:hover, +.typewriter-link:focus { + text-decoration: underline; +} + +.selected-tiff-tags { + display: flex; + flex-wrap: wrap; + align-items: center; /* Ensure tags are aligned in the middle */ + gap: 5px; /* Add some space between the tags */ + margin-top: 10px; +} + +.tiff-tag { + background-color: #007bff; + color: white; + padding: 2px 5px; /* Reduce padding for a more compact look */ + border-radius: 10px; /* Rounded corners for a chip-like appearance */ + font-size: 0.75em; /* Smaller font size for the tag */ + display: flex; + align-items: center; /* Center the content vertically */ + justify-content: center; /* Center the content horizontally */ + gap: 5px; /* Space between text and 'X' button */ + white-space: nowrap; /* Prevent wrapping inside the tag */ +} + +.tiff-tag .tiff-tag-remove { + display: inline-block; + width: 12px; /* Fixed width */ + height: 12px; /* Fixed height */ + line-height: 12px; /* Aligns the 'X' vertically */ + text-align: center; + background-color: transparent; + color: white; + border-radius: 50%; /* Circular button */ + padding: 0; + font-size: 12px; /* Adjust based on your design */ + cursor: pointer; +} + +.tiff-tag .tiff-tag-remove:hover { + background-color: rgba(255, 255, 255, 0.2); /* Slight highlight on hover */ +} + +@media (min-width: 768px) { + .mp4-player { + min-width: 596px; /* Adjust for tablets and small desktops */ + } +} + +@media (min-width: 1024px) { + .mp4-player { + min-width: 470px; /* Adjust for larger desktops */ + } + + body, + html { + overflow-x: initial; + } +} + +.histogram-description p { + font-size: 0.9em; + color: var( + --color-primary-dark + ); /* Assuming this is a darker shade for text */ + padding: 10px; + background-color: var(--color-background); + border-radius: 4px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin-top: 20px; +} + +.app-footer p { + margin: 10px 0; + font-size: 1rem; /* Adjust the font size as needed */ + line-height: 1.5; /* Improves readability */ + max-width: 80%; /* Limits the width of the text to improve readability */ + text-align: justify; /* Justifies the text for a neater appearance */ +} + +.app-footer a { + color: var( + --color-highlight + ); /* Highlight color for links, assuming you define this */ + text-decoration: underline; +} + +.app-footer a:hover { + color: var(--color-secondary); /* Color change on hover for visual feedback */ +} + +/* Additional styles for responsiveness */ +@media (max-width: 768px) { + .app-footer p { + font-size: 0.9rem; /* Slightly smaller font size for smaller screens */ + max-width: 95%; /* Increase width percentage for smaller screens */ + } +} + +.download-instructions ol { + margin-left: 20px; +} + +.download-button:enabled { + background-color: #4caf50; /* Green */ +} + +.download-button:disabled { + background-color: #ccc; + color: #666; + cursor: not-allowed; +} + +.selected-tiff-tags { + display: flex; + flex-wrap: wrap; + margin-top: 10px; +} + +.tiff-tag { + margin: 5px; + background-color: #007bff; + color: white; + padding: 5px; + border-radius: 5px; + cursor: pointer; +} + +.tiff-tag:hover { + opacity: 0.7; +} + +/* New class for horizontal side panel layout */ +.side-panel-horizontal { + display: flex; /* Use flexbox to layout items horizontally */ + flex-wrap: wrap; /* Allow items to wrap if needed */ + justify-content: space-around; /* Space out items evenly */ + padding: 10px; + margin-top: 20px; /* Add some space above the side panel */ + margin-left: -13px; +} + +/* Adjustments for panel items to display in a row */ +.side-panel-horizontal .panel-item { + flex: 1 1 25%; /* This makes each item take up a quarter of the horizontal space */ + max-width: calc(25% - 20px); /* Adjusts for margin */ + box-sizing: border-box; /* Includes padding and border in the element's total width and height */ + margin: 10px; /* Add some space around items */ + display: flex; /* Use flex to manage content inside each panel item */ + align-items: center; /* Centers items vertically inside the panel item */ +} + +/* Directly style .panel-content for horizontal layout */ +.side-panel-horizontal .panel-content { + display: flex; + flex-wrap: wrap; + justify-content: space-between; /* Adjust spacing to use available space */ + width: 100%; /* Ensure it takes full width of its parent */ +} + +.histogram-and-legend-container-contents { + flex-grow: 1; + width: 100%; +} + +.download-section { + flex-direction: column; + flex-wrap: wrap; +} + +.round-well-info { + width: 100%; +} + +.polar-plot-container { + width: 100%; +} +.round-well-info .well { + padding: auto; +} + +.deleter { + width: 100%; +} + +.polar-plot-description p { + font-size: 0.9em; + color: var( + --color-primary-dark + ); /* Assuming this is a darker shade for text */ + padding: 10px; + background-color: var(--color-background); + border-radius: 4px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin-top: 20px; +} + +/* Adjust for larger screens */ +@media (min-width: 1024px) { + .side-panel-horizontal { + width: 100%; + } +} + +/* New Independent Scrolling Styles */ +.sider-container { + width: 300px; + height: 100vh; + overflow-y: auto; + position: relative; + z-index: 1; + padding-bottom: 60px; +} + +.content-container { + flex: 1; + height: 100vh; + overflow-y: auto; + position: relative; +} + +/* Ensure proper collapse panel behavior */ +.ant-collapse-content { + overflow: visible !important; +} + +/* Add these new styles for the file listing container */ +.file-listing { + padding: 8px 0; + cursor: pointer; + color: #1890ff; + word-break: break-word; + overflow-wrap: break-word; + font-size: 0.9em; + line-height: 1.4; + border-bottom: 1px solid #f0f0f0; + transition: background-color 0.2s ease; + display: flex; + align-items: center; +} + +.file-listing:hover { + background-color: #f5f5f5; +} + +/* Style for the file name text */ +.file-name { + white-space: normal; + overflow: hidden; + text-overflow: ellipsis; + padding: 0 10px; + width: 100%; +} + +/* Update Ant Design's collapse panel styles */ +.ant-collapse-content-box { + padding: 0 !important; +} + +.ant-collapse-header { + font-weight: 500; + background-color: #fafafa; +} + +/* Add styles for the body part headers */ +.body-part-header { + font-weight: 500; + padding: 8px 12px; + background-color: #f5f5f5; + border-bottom: 1px solid #e8e8e8; + margin: 0; +} + +/* Add tooltip styles for long file names */ +.file-name-tooltip { + max-width: 300px; + word-wrap: break-word; + visibility: hidden; + background-color: rgba(0, 0, 0, 0.75); + color: #fff; + text-align: center; + padding: 5px 10px; + border-radius: 4px; + position: absolute; + z-index: 1500; + font-size: 0.85em; + left: 100%; + top: 50%; + transform: translateY(-50%); + opacity: 0; + transition: opacity 0.3s; +} + +.file-listing:hover .file-name-tooltip { + visibility: visible; + opacity: 1; +} + +/* Add styles for optional file icon */ +.file-icon { + min-width: 16px; + margin-right: 8px; + color: #1890ff; +} + +/* Ensure the sider content doesn't overflow */ +.ant-layout-sider-children { + overflow-x: hidden; +} + +/* Style scrollbar for better visibility */ +.ant-layout-sider::-webkit-scrollbar { + width: 6px; +} + +.ant-layout-sider::-webkit-scrollbar-thumb { + background-color: #d9d9d9; + border-radius: 3px; +} + +.ant-layout-sider::-webkit-scrollbar-track { + background-color: #f0f0f0; +} + +.plane-player-controls { + background: #fff; + border-radius: 8px; + padding: 15px; + margin: 10px 0; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.plane-player-controls .ant-slider { + margin: 10px 0; +} + +.plane-player-controls .ant-row { + margin: 10px 0; +} + +.plane-player-controls h4 { + margin-bottom: 15px; + text-align: center; +} + +.planar-slice-player { + background: #fff; + border-radius: 8px; + transition: height 0.3s ease; +} + +.player-header { + user-select: none; +} + +/* Base styles for planar controls container */ +.planar-controls-container { + position: absolute; + bottom: 10%; + left: 50%; + transform: translateX(-50%); + width: min(95%, 808px); + max-width: calc(100vw - 40px); + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(4px); + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + padding: 16px; + z-index: 100; + transition: all 0.3s ease; +} + +.planar-slice-player { + width: 100%; + max-width: 100%; + background: #fff; + border-radius: 8px; + transition: all 0.3s ease; +} + +/* Small screens */ +@media (max-width: 576px) { + .planar-controls-container { + width: 95%; + bottom: 5%; + padding: 12px; + } + + .planar-slice-player { + font-size: 0.9em; + } + + .planar-slice-player .ant-row { + margin: 8px 0; + } + + .planar-slice-player .ant-btn { + padding: 4px 8px; + font-size: 0.9em; + } +} + +/* Medium screens */ +@media (min-width: 577px) and (max-width: 992px) { + .planar-controls-container { + width: 90%; + bottom: 7%; + } +} + +/* Large screens */ +@media (min-width: 993px) { + .planar-controls-container { + width: min(85%, 808px); + } +} + +/* Handle touch devices */ +@media (hover: none) { + .planar-controls-container { + touch-action: none; + } + + .planar-slice-player input[type="number"] { + font-size: 16px; /* Prevents iOS zoom on focus */ + } +} + +.playerContainer { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: white; + border-top: 1px solid #e0e0e0; + box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease; + z-index: 1000; +} + +.collapsed { + transform: translateY(calc(100% - 48px)); +} + +.expanded { + transform: translateY(0); +} + +.toggleBar { + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background-color: #f5f5f5; + cursor: pointer; + transition: background-color 0.2s; +} + +.toggleBar:hover { + background-color: #e8e8e8; +} + +.toggleText { + width: 100%; + text-align: center; + font-size: 14px; + font-weight: 500; + color: #666; +} + +.playerContent { + padding: 16px; +} + +.controlsRow { + display: flex; + justify-content: center; + gap: 8px; + margin-bottom: 16px; +} + +.settingsRow { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 16px; +} + +.label { + font-size: 14px; + color: #666; +} + + +.advanced-mode-toggle { + display: flex; + align-items: center; + gap: 8px; + margin-top: 8px; + min-width: 0; + flex-wrap: nowrap; + white-space: nowrap; +} + +.advanced-mode-label { + font-size: 12px; + color: #666; + margin-left: 4px; + flex-shrink: 0; /* Prevents text from wrapping */ +} + +/* Responsive adjustments */ +@media (max-width: 400px) { + .advanced-mode-toggle { + flex-direction: row; + align-items: center; + justify-content: flex-start; + padding-right: 8px; + } +} + +/* Ensure switch maintains its size */ +:global(.ant-switch) { + flex-shrink: 0; + min-width: 28px; +} \ No newline at end of file diff --git a/src/App.js b/src/App.js index 3784575..42d7453 100644 --- a/src/App.js +++ b/src/App.js @@ -1,23 +1,46 @@ -import logo from './logo.svg'; +import logo from './components/ISAS_Logo_Standard.34684188-1.svg'; import './App.css'; +import { BrowserRouter as Router, Route, Routes, Link } from 'react-router-dom'; +import WelcomePage from './components/WelcomePage'; +import NotFoundPage from './components/NotFoundPage'; +import Footer from './components/Footer'; + +import VolumeViewer from './components/VolumeViewer'; function App() { + const headerStyle = { + backgroundColor: 'black', + color: 'white', + padding: '10px', + textAlign: 'center', + position: 'fixed', + top: 0, + left: 0, + right: 0, + zIndex: 1000 + }; + return (
-
- logo -

- Edit src/App.js and save to reload. -

- - Learn React - -
+ +
+ +
+ {/* Wrapping the Routes and Footer within a flex container */} +
+ + } /> + } /> + } /> + +
+
); } diff --git a/src/components/ClipRegionSlider.js b/src/components/ClipRegionSlider.js new file mode 100644 index 0000000..c84ed15 --- /dev/null +++ b/src/components/ClipRegionSlider.js @@ -0,0 +1,123 @@ +import React, { useCallback, useMemo } from "react"; +import { Slider } from "antd"; + +const ClipRegionSlider = ({ axis, onChange, value = [0, 1], totalSlices }) => { + // Memoize conversion functions + const toSliceValue = useCallback( + (val) => + Math.max( + 1, + Math.min(totalSlices, Math.round(val * (totalSlices - 1) + 1)), + ), + [totalSlices], + ); + + const fromSliceValue = useCallback( + (slice) => (slice - 1) / (totalSlices - 1), + [totalSlices], + ); + + // Memoize slider values and marks + const sliceValues = useMemo( + () => [toSliceValue(value[0]), toSliceValue(value[1])], + [value, toSliceValue], + ); + + const marks = useMemo( + () => ({ + 1: "1", + [Math.floor((totalSlices + 1) / 2)]: axis, + [totalSlices]: totalSlices.toString(), + }), + [totalSlices, axis], + ); + + // Memoize tooltip formatter + const tooltipFormatter = useCallback((value) => `Slice ${value}`, []); + + // Debounced onChange handler + const handleChange = useCallback( + (newSliceValues) => { + // Skip if values haven't changed + if ( + newSliceValues[0] === sliceValues[0] && + newSliceValues[1] === sliceValues[1] + ) { + return; + } + + // Use RequestAnimationFrame to throttle updates + requestAnimationFrame(() => { + onChange([ + fromSliceValue(newSliceValues[0]), + fromSliceValue(newSliceValues[1]), + ]); + }); + }, + [fromSliceValue, sliceValues, onChange], + ); + + const sliderProps = useMemo( + () => ({ + range: true, + marks, + min: 1, + max: totalSlices, + step: 1, + value: sliceValues, + onChange: handleChange, + tooltip: { + formatter: tooltipFormatter, + open: undefined, // Let antd handle tooltip visibility + placement: "top", + }, + handleStyle: [ + { backgroundColor: "#1890ff", border: "2px solid #1890ff" }, + { backgroundColor: "#1890ff", border: "2px solid #1890ff" }, + ], + trackStyle: [{ backgroundColor: "#91d5ff" }], + }), + [marks, totalSlices, sliceValues, handleChange, tooltipFormatter], + ); + + return ( +
+ + +
+ ); +}; + +export default React.memo(ClipRegionSlider); diff --git a/src/components/DatasetCard.js b/src/components/DatasetCard.js new file mode 100755 index 0000000..d191bb1 --- /dev/null +++ b/src/components/DatasetCard.js @@ -0,0 +1,44 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; + +function DatasetCard({ title, description, datasetId }) { + const navigate = useNavigate(); + + const cardStyle = { + display: 'flex', + flexDirection: 'column', + margin: '10px', + padding: '20px', + border: '1px solid black', + borderRadius: '5px', + backgroundColor: 'white', + boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)', // Shadow for depth + transition: 'transform 0.2s' // Smooth transition for hover effect + }; + + const buttonStyle = { + marginTop: '10px', + backgroundColor: 'black', + color: 'white', + border: 'none', + padding: '10px 20px', + borderRadius: '4px', + cursor: 'pointer', + textAlign: 'center' + }; + + const handleLoadClick = () => { + navigate(`/dataset/${datasetId}`); + }; + + return ( +
e.currentTarget.style.transform = 'scale(1.05)'} onMouseOut={e => e.currentTarget.style.transform = 'none'}> +
{title}
+

+
{description}
+ +
+ ); +} + +export default DatasetCard; diff --git a/src/components/FilesList.js b/src/components/FilesList.js new file mode 100644 index 0000000..1eda4f6 --- /dev/null +++ b/src/components/FilesList.js @@ -0,0 +1,185 @@ +// src/components/FilesList.js +import React from "react"; +import { Collapse, Tooltip } from "antd"; + +export default function FilesList({ fileData, onFileSelect }) { + if (!fileData || Object.keys(fileData).length === 0) { + return
No files available
; + } + + const formatSize = (bytes) => { + const units = ["B", "KB", "MB", "GB"]; + let size = bytes; + let unitIndex = 0; + + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + + return `${size.toFixed(2)} ${units[unitIndex]}`; + }; + + const getTooltipContent = (file) => ( +
+
+ Name: {file.name} +
+
+ Type: {file.type.toUpperCase()} +
+ {file.size && ( +
+ Size: {formatSize(file.size)} +
+ )} +
+ Category: {file.category} +
+ {file.lastModified && ( +
+ Last Modified:{" "} + {new Date(file.lastModified).toLocaleString()} +
+ )} +
+ ); + + return ( + + {Object.entries(fileData).map(([category, files]) => ( + {category}} + key={category} + > + {Array.isArray(files) ? ( + files.map((file) => { + const fileObj = + typeof file === "string" + ? { + name: file, + type: file.endsWith(".zarr") ? "zarr" : "tiff", + size: 0, + category, + } + : { ...file, category }; + + return ( + +
onFileSelect(category, fileObj)} + > + + {fileObj.type === "zarr" ? "📁" : "📄"} + + {fileObj.name} + + + {fileObj.type.toUpperCase()} + + {fileObj.size > 0 && ( + + {formatSize(fileObj.size)} + + )} + +
+
+ ); + }) + ) : ( +
Invalid files data for category: {category}
+ )} +
+ ))} + + + + {/* Global styles for tooltip */} + +
+ ); +} diff --git a/src/components/Footer.js b/src/components/Footer.js new file mode 100755 index 0000000..4fe4921 --- /dev/null +++ b/src/components/Footer.js @@ -0,0 +1,49 @@ +import React from "react"; + +const Footer = () => { + return ( + + ); +}; + +export default Footer; diff --git a/src/components/ISAS_Logo_Standard.34684188-1.svg b/src/components/ISAS_Logo_Standard.34684188-1.svg new file mode 100755 index 0000000..d51e69a --- /dev/null +++ b/src/components/ISAS_Logo_Standard.34684188-1.svg @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/NotFoundPage.js b/src/components/NotFoundPage.js new file mode 100755 index 0000000..1e93b47 --- /dev/null +++ b/src/components/NotFoundPage.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import '../App.css'; // Assuming you have a dedicated CSS file for this component + +function NotFoundPage() { + return ( +
+
+
+
+
+
+ {/* Repeat for more planets/orbits */} +
+
+

404

+

Lost in the cosmic void...

+ Navigate Back Home +
+
+ ); +} + +export default NotFoundPage; \ No newline at end of file diff --git a/src/components/PlanarSlicePlayer.js b/src/components/PlanarSlicePlayer.js new file mode 100644 index 0000000..e0c03fc --- /dev/null +++ b/src/components/PlanarSlicePlayer.js @@ -0,0 +1,340 @@ +// PlanarSlicePlayer.js +import React, { useState, useEffect, useCallback, useRef } from "react"; +import { Button, Slider, InputNumber, Typography, Switch, Tooltip } from "antd"; +import { + PlayCircleOutlined, + PauseCircleOutlined, + StopOutlined, + StepForwardOutlined, + StepBackwardOutlined, +} from "@ant-design/icons"; + +const { Text } = Typography; + +const PlanarSlicePlayer = ({ + currentVolume, + cameraMode, + updateClipRegion, + clipRegion, + onSliceChange, +}) => { + const [isPlaying, setIsPlaying] = useState(false); + const [playbackSpeed, setPlaybackSpeed] = useState(1); + const [currentSlice, setCurrentSlice] = useState(0); + const [totalSlices, setTotalSlices] = useState(100); + const [isLooping, setIsLooping] = useState(true); + const playbackIntervalRef = useRef(null); + const currentSliceRef = useRef(currentSlice); + + useEffect(() => { + currentSliceRef.current = currentSlice; + }, [currentSlice]); + + const getAxisInfo = useCallback(() => { + switch (cameraMode) { + case "X": + return { + min: "xmin", + max: "xmax", + label: "X", + size: currentVolume?.imageInfo?.volumeSize?.x, + }; + case "Y": + return { + min: "ymin", + max: "ymax", + label: "Y", + size: currentVolume?.imageInfo?.volumeSize?.y, + }; + case "Z": + return { + min: "zmin", + max: "zmax", + label: "Z", + size: currentVolume?.imageInfo?.volumeSize?.z, + }; + default: + return null; + } + }, [cameraMode, currentVolume]); + + const updateSlice = useCallback( + (newSlice) => { + if (!currentVolume) return; + + const axisInfo = getAxisInfo(); + if (!axisInfo) return; + + const normalizedPos = newSlice / (totalSlices - 1); + const sliceThickness = 0.01; + + const newClipRegion = { + ...clipRegion, + [axisInfo.min]: Math.max(0, normalizedPos - sliceThickness / 2), + [axisInfo.max]: Math.min(1, normalizedPos + sliceThickness / 2), + }; + + updateClipRegion(newClipRegion); + setCurrentSlice(newSlice); + onSliceChange?.(newSlice); + }, + [ + currentVolume, + getAxisInfo, + clipRegion, + totalSlices, + updateClipRegion, + onSliceChange, + ], + ); + + const stopPlayback = useCallback(() => { + if (playbackIntervalRef.current) { + clearInterval(playbackIntervalRef.current); + playbackIntervalRef.current = null; + } + setIsPlaying(false); + }, []); + + const startPlayback = useCallback(() => { + stopPlayback(); + setIsPlaying(true); + + playbackIntervalRef.current = setInterval(() => { + const nextSlice = currentSliceRef.current + 1; + + if (nextSlice >= totalSlices) { + if (isLooping) { + updateSlice(0); + } else { + stopPlayback(); + } + return; + } + + updateSlice(nextSlice); + }, 1000 / playbackSpeed); + }, [playbackSpeed, totalSlices, updateSlice, stopPlayback, isLooping]); + + const pausePlayback = useCallback(() => { + stopPlayback(); + }, [stopPlayback]); + + const resetToStart = useCallback(() => { + stopPlayback(); + updateSlice(0); + }, [stopPlayback, updateSlice]); + + const stepForward = useCallback(() => { + const nextSlice = Math.min(currentSliceRef.current + 1, totalSlices - 1); + updateSlice(nextSlice); + }, [totalSlices, updateSlice]); + + const stepBackward = useCallback(() => { + const prevSlice = Math.max(currentSliceRef.current - 1, 0); + updateSlice(prevSlice); + }, [updateSlice]); + + useEffect(() => { + const axisInfo = getAxisInfo(); + if (axisInfo?.size) { + setTotalSlices(axisInfo.size); + setCurrentSlice((prev) => Math.min(prev, axisInfo.size - 1)); + } + }, [getAxisInfo]); + + useEffect(() => { + return () => { + if (playbackIntervalRef.current) { + clearInterval(playbackIntervalRef.current); + } + }; + }, []); + + useEffect(() => { + stopPlayback(); + setCurrentSlice(0); + }, [cameraMode, stopPlayback]); + + useEffect(() => { + if (isPlaying) { + startPlayback(); + } + }, [playbackSpeed, startPlayback, isPlaying]); + + const axisInfo = getAxisInfo(); + if (!axisInfo) return null; + + return ( +
+
+ +
+ {`${axisInfo.label} Plane`} +
+
+ +
+ + + ) : ( + + )} + + + + + + + +
+ +
+ + `Slice ${value + 1} of ${totalSlices}`, + }} + /> + +
+ +
+ +
+ Slice: + + / {totalSlices - 1} +
+
+ + +
+ Speed (fps): + +
+
+ + +
+ Loop: + +
+
+
+
+ +
+ ); +}; + +export default PlanarSlicePlayer; diff --git a/src/components/PlanarSlicePlayer.module.css b/src/components/PlanarSlicePlayer.module.css new file mode 100644 index 0000000..ca950f3 --- /dev/null +++ b/src/components/PlanarSlicePlayer.module.css @@ -0,0 +1,65 @@ +/* PlanarSlicePlayer.module.css */ +.playerContainer { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: white; + border-top: 1px solid #e0e0e0; + box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease; + z-index: 1000; +} + +.collapsed { + transform: translateY(calc(100% - 48px)); +} + +.expanded { + transform: translateY(0); +} + +.toggleBar { + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background-color: #f5f5f5; + cursor: pointer; + transition: background-color 0.2s; +} + +.toggleBar:hover { + background-color: #e8e8e8; +} + +.toggleText { + width: 100%; + text-align: center; + font-size: 14px; + font-weight: 500; + color: #666; +} + +.playerContent { + padding: 16px; +} + +.controlsRow { + display: flex; + justify-content: center; + gap: 8px; + margin-bottom: 16px; +} + +.settingsRow { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 16px; +} + +.label { + font-size: 14px; + color: #666; +} diff --git a/src/components/PlanarSliceWindow.js b/src/components/PlanarSliceWindow.js new file mode 100644 index 0000000..64c409f --- /dev/null +++ b/src/components/PlanarSliceWindow.js @@ -0,0 +1,270 @@ +import React, { useState, useRef, useEffect } from "react"; +import { Button, Card } from "antd"; +import { + FullscreenOutlined, + FullscreenExitOutlined, + MinusOutlined, + CloseOutlined, +} from "@ant-design/icons"; + +const PlanarSliceWindow = ({ children, onClose, mode }) => { + const [isMinimized, setIsMinimized] = useState(false); + const [isMaximized, setIsMaximized] = useState(false); + const [position, setPosition] = useState({ x: 20, y: 20 }); + const [size, setSize] = useState({ width: 600, height: 400 }); + const [isDragging, setIsDragging] = useState(false); + const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); + const [isResizing, setIsResizing] = useState(false); + const [resizeEdge, setResizeEdge] = useState(null); + const [originalSize, setOriginalSize] = useState(null); + const [originalPosition, setOriginalPosition] = useState(null); + + const windowRef = useRef(null); + const dragStartPos = useRef({ x: 0, y: 0 }); + + useEffect(() => { + const parent = document.getElementById("volume-viewer"); + if (parent) { + const rect = parent.getBoundingClientRect(); + setPosition({ + x: (rect.width - size.width) / 2, + y: (rect.height - size.height) / 2, + }); + } + }, [size.width, size.height]); + + const handleMouseDown = (e) => { + if (e.target.closest(".resize-handle") || isMaximized) return; + + setIsDragging(true); + const rect = windowRef.current.getBoundingClientRect(); + setDragOffset({ + x: e.clientX - rect.left, + y: e.clientY - rect.top, + }); + dragStartPos.current = { x: e.clientX, y: e.clientY }; + }; + + const handleMouseMove = (e) => { + if (!isDragging && !isResizing) return; + + if (isDragging) { + const parent = document.getElementById("volume-viewer"); + const parentRect = parent.getBoundingClientRect(); + + let newX = e.clientX - dragOffset.x; + let newY = e.clientY - dragOffset.y; + + newX = Math.max(0, Math.min(newX, parentRect.width - size.width)); + newY = Math.max(0, Math.min(newY, parentRect.height - size.height)); + + setPosition({ x: newX, y: newY }); + } + + if (isResizing) { + const parent = document.getElementById("volume-viewer"); + const parentRect = parent.getBoundingClientRect(); + const minWidth = 400; + const minHeight = 300; + + let newWidth = size.width; + let newHeight = size.height; + let newX = position.x; + let newY = position.y; + + const deltaX = e.clientX - dragStartPos.current.x; + const deltaY = e.clientY - dragStartPos.current.y; + + if (resizeEdge.includes("right")) { + newWidth = Math.max(minWidth, size.width + deltaX); + newWidth = Math.min(newWidth, parentRect.width - position.x); + } + if (resizeEdge.includes("bottom")) { + newHeight = Math.max(minHeight, size.height + deltaY); + newHeight = Math.min(newHeight, parentRect.height - position.y); + } + if (resizeEdge.includes("left")) { + const possibleWidth = Math.max(minWidth, size.width - deltaX); + if (possibleWidth !== size.width) { + newX = Math.max(0, position.x + deltaX); + newWidth = possibleWidth; + } + } + if (resizeEdge.includes("top")) { + const possibleHeight = Math.max(minHeight, size.height - deltaY); + if (possibleHeight !== size.height) { + newY = Math.max(0, position.y + deltaY); + newHeight = possibleHeight; + } + } + + setSize({ width: newWidth, height: newHeight }); + setPosition({ x: newX, y: newY }); + } + }; + + const handleMouseUp = () => { + setIsDragging(false); + setIsResizing(false); + }; + + const handleResizeStart = (e, edge) => { + e.stopPropagation(); + setIsResizing(true); + setResizeEdge(edge); + dragStartPos.current = { x: e.clientX, y: e.clientY }; + }; + + const toggleMinimize = () => { + setIsMinimized(!isMinimized); + }; + + const toggleMaximize = () => { + if (!isMaximized) { + setOriginalSize({ ...size }); + setOriginalPosition({ ...position }); + + const parent = document.getElementById("volume-viewer"); + const parentRect = parent.getBoundingClientRect(); + + setSize({ + width: parentRect.width, + height: parentRect.height, + }); + setPosition({ x: 0, y: 0 }); + } else { + setSize(originalSize); + setPosition(originalPosition); + } + setIsMaximized(!isMaximized); + }; + + useEffect(() => { + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseup", handleMouseUp); + return () => { + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); + }; + }, [isDragging, isResizing]); + + const resizeHandles = [ + { position: "top", cursor: "ns-resize" }, + { position: "right", cursor: "ew-resize" }, + { position: "bottom", cursor: "ns-resize" }, + { position: "left", cursor: "ew-resize" }, + { position: "top-left", cursor: "nw-resize" }, + { position: "top-right", cursor: "ne-resize" }, + { position: "bottom-left", cursor: "sw-resize" }, + { position: "bottom-right", cursor: "se-resize" }, + ]; + + return ( + +
+ + Planar Slice Player - {mode} View + +
+
+
+ +
+ {children} +
+ + {!isMinimized && + !isMaximized && + resizeHandles.map((handle) => ( +
handleResizeStart(e, handle.position)} + /> + ))} + + ); +}; + +export default PlanarSliceWindow; diff --git a/src/components/ResizablePlanarPlayer.js b/src/components/ResizablePlanarPlayer.js new file mode 100644 index 0000000..b204053 --- /dev/null +++ b/src/components/ResizablePlanarPlayer.js @@ -0,0 +1,311 @@ +import React, { useState, useRef, useEffect } from "react"; +import { + GripHorizontal, + ChevronUp, + ChevronDown, + ArrowLeftCircle, + ArrowRightCircle, + Play, + Pause, + StopCircle, +} from "lucide-react"; +import { Button, Slider, InputNumber, Switch, Card } from "antd"; + +const ResizablePlanarPlayer = ({ + currentVolume, + cameraMode, + updateClipRegion, + clipRegion, + onSliceChange, +}) => { + const [isMinimized, setIsMinimized] = useState(false); + const [position, setPosition] = useState({ + x: 20, + y: window.innerHeight - 400, + }); + const [size, setSize] = useState({ width: 400, height: 300 }); + const [isDragging, setIsDragging] = useState(false); + const [isResizing, setIsResizing] = useState(false); + const [isPlaying, setIsPlaying] = useState(false); + const [currentSlice, setCurrentSlice] = useState(0); + const [playbackSpeed, setPlaybackSpeed] = useState(1); + const [isLooping, setIsLooping] = useState(true); + const [totalSlices, setTotalSlices] = useState(100); + + const playerRef = useRef(null); + const dragStartRef = useRef({ x: 0, y: 0 }); + const playbackIntervalRef = useRef(null); + + useEffect(() => { + if (currentVolume?.imageInfo) { + const dimension = + cameraMode === "X" ? "sizeX" : cameraMode === "Y" ? "sizeY" : "sizeZ"; + setTotalSlices(currentVolume.imageInfo[dimension] || 100); + } + }, [currentVolume, cameraMode]); + + useEffect(() => { + return () => { + if (playbackIntervalRef.current) { + clearInterval(playbackIntervalRef.current); + } + }; + }, []); + + const handleDragStart = (e) => { + if (e.target.closest(".resize-handle")) return; + setIsDragging(true); + dragStartRef.current = { + x: e.clientX - position.x, + y: e.clientY - position.y, + }; + }; + + const handleDrag = (e) => { + if (!isDragging) return; + + const newX = Math.max( + 0, + Math.min( + window.innerWidth - size.width, + e.clientX - dragStartRef.current.x, + ), + ); + const newY = Math.max( + 0, + Math.min( + window.innerHeight - size.height, + e.clientY - dragStartRef.current.y, + ), + ); + + setPosition({ x: newX, y: newY }); + }; + + const handleResizeStart = (e) => { + e.stopPropagation(); + setIsResizing(true); + dragStartRef.current = { + x: e.clientX, + y: e.clientY, + width: size.width, + height: size.height, + }; + }; + + const handleResize = (e) => { + if (!isResizing) return; + + const minWidth = 300; + const minHeight = 200; + const maxWidth = window.innerWidth - position.x; + const maxHeight = window.innerHeight - position.y; + + const newWidth = Math.max( + minWidth, + Math.min( + maxWidth, + dragStartRef.current.width + (e.clientX - dragStartRef.current.x), + ), + ); + const newHeight = Math.max( + minHeight, + Math.min( + maxHeight, + dragStartRef.current.height + (e.clientY - dragStartRef.current.y), + ), + ); + + setSize({ width: newWidth, height: newHeight }); + }; + + const updateSlice = (newSlice) => { + if (!currentVolume) return; + + const axisInfo = { + min: cameraMode === "X" ? "xmin" : cameraMode === "Y" ? "ymin" : "zmin", + max: cameraMode === "X" ? "xmax" : cameraMode === "Y" ? "ymax" : "zmax", + }; + + const normalizedPos = newSlice / (totalSlices - 1); + const sliceThickness = 0.01; + + const newClipRegion = { + ...clipRegion, + [axisInfo.min]: Math.max(0, normalizedPos - sliceThickness / 2), + [axisInfo.max]: Math.min(1, normalizedPos + sliceThickness / 2), + }; + + updateClipRegion(newClipRegion); + setCurrentSlice(newSlice); + onSliceChange?.(newSlice); + }; + + const togglePlayback = () => { + if (isPlaying) { + clearInterval(playbackIntervalRef.current); + setIsPlaying(false); + } else { + setIsPlaying(true); + playbackIntervalRef.current = setInterval(() => { + setCurrentSlice((prev) => { + const next = prev + 1; + if (next >= totalSlices) { + if (isLooping) { + updateSlice(0); + return 0; + } else { + clearInterval(playbackIntervalRef.current); + setIsPlaying(false); + return prev; + } + } + updateSlice(next); + return next; + }); + }, 1000 / playbackSpeed); + } + }; + + const stopPlayback = () => { + clearInterval(playbackIntervalRef.current); + setIsPlaying(false); + setCurrentSlice(0); + updateSlice(0); + }; + + return ( + { + setIsDragging(false); + setIsResizing(false); + }} + onMouseLeave={() => { + setIsDragging(false); + setIsResizing(false); + }} + > +
+
+ + + {cameraMode} Plane Control + +
+
+ + {!isMinimized && ( +
+
+
+ + +
+ +
+
+ +
+
+ Speed (fps): + +
+
+ Loop Playback: + +
+
+
+
+ )} +
+ ); +}; + +export default ResizablePlanarPlayer; diff --git a/src/components/SidebarTabs.js b/src/components/SidebarTabs.js new file mode 100644 index 0000000..03992c5 --- /dev/null +++ b/src/components/SidebarTabs.js @@ -0,0 +1,604 @@ +import React, { useState } from 'react'; +import { Layout, Tabs, Collapse, Switch, Slider, InputNumber, Row, Col, Select, Input, Button, Tooltip } from 'antd'; +import { + SettingOutlined, + FolderOutlined, + EyeOutlined, + CameraOutlined, + ControlOutlined, + BgColorsOutlined, + ApartmentOutlined, + BorderOutlined, + FileImageOutlined, + BulbOutlined // Changed from LightOutlined +} from '@ant-design/icons'; +import { PRESET_COLOR_MAP } from './constants'; + +const { Sider } = Layout; +const { TabPane } = Tabs; +const { Panel } = Collapse; +const { Option } = Select; + +export const SidebarTabs = ({ + settings, + updateSetting, + channels, + updateChannelOptions, + clipRegion, + updateClipRegion, + fileData, + handleFileSelect, + metadata, + lights, + updateLights, + currentPreset, + applyColorPreset +}) => { + const [activeKey, setActiveKey] = useState('settings'); + + // Helper function to format color for input + const rgbToHex = (r, g, b) => { + const toHex = x => ('0' + Math.round(x).toString(16)).slice(-2); + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; + }; + + // Helper function to parse hex color + const hexToRgb = (hex) => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? [ + parseInt(result[1], 16), + parseInt(result[2], 16), + parseInt(result[3], 16) + ] : null; + }; + + return ( + + + {/* Settings Tab */} + + + + } + key="settings" + > + + {/* Render Settings */} + + Render Mode + + } + key="render" + > + + Path Trace + + updateSetting('pathTrace', val)} + /> + + + + Density + + updateSetting('density', val)} + /> + + + + Brightness + + updateSetting('brightness', val)} + /> + + + + Mask Alpha + + updateSetting('maskAlpha', val)} + /> + + + + + {/* Camera Settings */} + + Camera + + } + key="camera" + > + + Mode + + + + + + FOV + + updateSetting('fov', val)} + style={{ width: '100%' }} + /> + + + + Focal Distance + + updateSetting('focalDistance', val)} + style={{ width: '100%' }} + /> + + + + Aperture + + updateSetting('aperture', val)} + style={{ width: '100%' }} + /> + + + + + {/* Display Settings */} + + Display + + } + key="display" + > + + Show Axis + + updateSetting('showAxis', val)} + /> + + + + Show Scale Bar + + updateSetting('showScaleBar', val)} + /> + + + + Show Bounding Box + + updateSetting('showBoundingBox', val)} + /> + + + + Background Color + + { + const rgb = hexToRgb(e.target.value); + if (rgb) updateSetting('backgroundColor', rgb); + }} + /> + + + + + {/* Channel Settings */} + + Channels + + } + key="channels" + > + + Color Preset + + + + + {channels.map((channel, index) => ( +
+

{channel.name || `Channel ${index + 1}`}

+ + Enable + + updateChannelOptions(index, { enabled: val })} + /> + + + + Opacity + + updateChannelOptions(index, { opacity: val })} + /> + + + + Color + + { + const rgb = hexToRgb(e.target.value); + if (rgb) updateChannelOptions(index, { color: rgb }); + }} + /> + + + + Isosurface + + updateChannelOptions(index, { + isosurfaceEnabled: val + })} + /> + + + {channel.isosurfaceEnabled && ( + <> + + Isovalue + + updateChannelOptions(index, { + isovalue: val + })} + /> + + + + Surface Opacity + + updateChannelOptions(index, { + isosurfaceOpacity: val + })} + /> + + + + )} +
+ ))} +
+ + {/* Clipping Settings */} + + Clipping + + } + key="clipping" + > + {['X', 'Y', 'Z'].map(axis => ( +
+

{axis} Axis Clipping

+ { + updateClipRegion({ + ...clipRegion, + [`${axis.toLowerCase()}min`]: min, + [`${axis.toLowerCase()}max`]: max + }); + }} + /> +
+ ))} +
+ + {/* Lighting Settings */} + + Lighting + + } + key="lighting" + > + {/* Sky Light Controls */} +
+

Sky Light

+ {['Top', 'Middle', 'Bottom'].map((position, index) => ( +
+
{position}
+ + Intensity + + updateLights('sky', position.toLowerCase(), 'intensity', val)} + /> + + + + Color + + { + const rgb = hexToRgb(e.target.value); + if (rgb) updateLights('sky', position.toLowerCase(), 'color', rgb); + }} + /> + + +
+ ))} +
+ + {/* Area Light Controls */} +
+

Area Light

+ + Intensity + + updateLights('area', null, 'intensity', val)} + /> + + + + Color + + { + const rgb = hexToRgb(e.target.value); + if (rgb) updateLights('area', null, 'color', rgb); + }} + /> + + + + Direction (θ) + + updateLights('area', null, 'theta', val * (Math.PI / 180))} + /> + + + + Direction (φ) + + updateLights('area', null, 'phi', val * (Math.PI / 180))} + /> + + +
+
+ + {/* Metadata Panel */} + + Metadata + + } + key="metadata" + > + {metadata ? ( +
+ + Name: + {metadata.name} + + + Dimensions: + + {`${metadata.dimensions.x} × ${metadata.dimensions.y} × ${metadata.dimensions.z}`} + + + + Channels: + {metadata.dimensions.channels} + + + Pixel Size: + + {metadata.pixelSize.map(size => size.toFixed(2)).join(' × ')} {metadata.spatialUnit} + + +
+ ) : ( +
No volume loaded
+ )} +
+
+
+ + {/* Files Tab */} + + + + } + key="files" + > +
+ + {Object.entries(fileData).map(([category, files]) => ( + + {category} + + } + key={category} + > + {files.map(file => ( +
handleFileSelect(category, file)} + > + {file} +
+ ))} +
+ ))} +
+
+
+
+ + +
+ ); +}; + +export default SidebarTabs; \ No newline at end of file diff --git a/src/components/ThreePointGammaSlider.js b/src/components/ThreePointGammaSlider.js new file mode 100644 index 0000000..e6addbe --- /dev/null +++ b/src/components/ThreePointGammaSlider.js @@ -0,0 +1,55 @@ +import React from "react"; +import { Slider } from "antd"; + +const ThreePointGammaSlider = ({ onChange, value = [0, 128, 255] }) => { + const marks = { + 0: "Min", + 128: "Mid", + 255: "Max", + }; + + return ( +
+ + +
+ ); +}; + +export default ThreePointGammaSlider; diff --git a/src/components/TransferFunctionEditor.js b/src/components/TransferFunctionEditor.js new file mode 100644 index 0000000..3154559 --- /dev/null +++ b/src/components/TransferFunctionEditor.js @@ -0,0 +1,572 @@ +// src/components/TransferFunctionEditor.js + +import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react'; +import * as d3 from 'd3'; +import { Lut } from "@aics/volume-viewer"; + +const MARGIN = { + top: 30, + right: 10, + bottom: 35, + left: 55 +}; + +const TransferFunctionEditor = ({ + channelIndex, + histogram, + onLutUpdate, + width = 380, + height = 220, + initialControlPoints, + useAdvancedMode = false, + channelColor = [255, 255, 255], + rampRange: externalRampRange, + onRampRangeChange, + onControlPointsChange +}) => { + const svgRef = useRef(null); + const [controlPoints, setControlPoints] = useState(initialControlPoints || [ + { x: 0, opacity: 0, color: channelColor }, + { x: 255, opacity: 1, color: channelColor } + ]); + const [internalRampRange, setInternalRampRange] = useState(externalRampRange || [0, 255]); + const [selectedPoint, setSelectedPoint] = useState(null); + const [dragging, setDragging] = useState(false); + + const innerWidth = width - MARGIN.left - MARGIN.right; + const innerHeight = height - MARGIN.top - MARGIN.bottom; + + // Scale functions + const xScale = useMemo(() => + d3.scaleLinear() + .domain([0, 255]) + .range([0, innerWidth]) + .nice(), + [innerWidth] + ); + + const yScale = useMemo(() => + d3.scaleLinear() + .domain([-0.05, 1.05]) + .range([innerHeight, 0]), + [innerHeight] + ); + + // Histogram y-scale with log transform + const histogramYScale = useMemo(() => { + if (!histogram) return null; + + let maxValue = 0; + for (let i = 0; i < histogram.getNumBins(); i++) { + maxValue = Math.max(maxValue, histogram.getBin(i)); + } + + return d3.scaleLog() + .domain([1, maxValue]) + .range([innerHeight, 0]) + .nice(); + }, [histogram, innerHeight]); + + // Create gradient definition + const createGradientDef = useCallback((points) => { + const range = points[points.length - 1].x - points[0].x; + return points.map((cp, i) => { + const offset = `${((cp.x - points[0].x) / range) * 100}%`; + const opacity = Math.min(cp.opacity, 0.9); + return ; + }); + }, []); + + // Sync with external changes + useEffect(() => { + if (externalRampRange) { + setInternalRampRange(externalRampRange); + } + }, [externalRampRange]); + + useEffect(() => { + if (initialControlPoints) { + setControlPoints(initialControlPoints); + } + }, [initialControlPoints]); + + // Draw histogram + const drawHistogram = useCallback(() => { + if (!histogram || !svgRef.current || !histogramYScale) return; + + const binData = Array.from({length: histogram.getNumBins()}, (_, i) => ({ + bin: i, + value: histogram.getBin(i) + })); + + const svg = d3.select(svgRef.current); + const g = svg.select('.histogram-group'); + + const barWidth = Math.max(1, (innerWidth / histogram.getNumBins()) - 1); + + const bars = g.selectAll('.histogram-bar') + .data(binData); + + bars.enter() + .append('rect') + .attr('class', 'histogram-bar') + .merge(bars) + .attr('x', d => xScale(d.bin)) + .attr('y', d => histogramYScale(Math.max(1, d.value))) + .attr('width', barWidth) + .attr('height', d => innerHeight - histogramYScale(Math.max(1, d.value))) + .attr('fill', '#666') + .attr('opacity', 0.5); + + bars.exit().remove(); + }, [histogram, innerWidth, xScale, histogramYScale, innerHeight]); + + // Update transfer function visualization + const updateVisualization = useCallback(() => { + if (!svgRef.current) return; + + const svg = d3.select(svgRef.current); + const controlGroup = svg.select('.control-points-group'); + + // Clear existing elements + controlGroup.selectAll('*').remove(); + + // Add grid lines + controlGroup.append('g') + .attr('class', 'grid-lines') + .selectAll('line') + .data(yScale.ticks(5)) + .enter() + .append('line') + .attr('x1', 0) + .attr('x2', innerWidth) + .attr('y1', d => yScale(d)) + .attr('y2', d => yScale(d)) + .attr('stroke', '#ddd') + .attr('stroke-dasharray', '2,2'); + + // Create gradient definition + const gradientId = `tf-gradient-${channelIndex}`; + const defs = svg.selectAll('defs').data([null]).join('defs'); + + if (useAdvancedMode) { + defs.html(` + + ${createGradientDef(controlPoints).map(stop => + `` + ).join('')} + + `); + + // Draw filled area with gradient + const area = d3.area() + .x(d => xScale(d.x)) + .y0(innerHeight) + .y1(d => yScale(d.opacity)) + .curve(d3.curveLinear); + + controlGroup.append('path') + .attr('class', 'gradient-area') + .attr('d', area(controlPoints)) + .attr('fill', `url(#${gradientId})`) + .attr('opacity', 0.85); + + // Draw control point line + const line = d3.line() + .x(d => xScale(d.x)) + .y(d => yScale(d.opacity)) + .curve(d3.curveLinear); + + controlGroup.append('path') + .datum(controlPoints) + .attr('class', 'control-line') + .attr('fill', 'none') + .attr('stroke', `rgb(${channelColor.join(',')})`) + .attr('stroke-width', 2.5) + .attr('d', line); + + // Draw control points + controlGroup.selectAll('.control-point') + .data(controlPoints) + .enter() + .append('circle') + .attr('class', 'control-point') + .attr('cx', d => xScale(d.x)) + .attr('cy', d => yScale(d.opacity)) + .attr('r', 6) + .attr('fill', '#fff') + .attr('stroke', `rgb(${channelColor.join(',')})`) + .attr('stroke-width', 2); + } else { + // Basic mode visualization + const rampPoints = [ + { x: 0, opacity: 0, color: channelColor }, + { x: internalRampRange[0], opacity: 0, color: channelColor }, + { x: internalRampRange[1], opacity: 1, color: channelColor }, + { x: 255, opacity: 1, color: channelColor } + ]; + + defs.html(` + + ${createGradientDef(rampPoints).map(stop => + `` + ).join('')} + + `); + + // Draw vertical guidelines + controlGroup.selectAll('.ramp-guideline') + .data(internalRampRange) + .enter() + .append('line') + .attr('class', 'ramp-guideline') + .attr('x1', d => xScale(d)) + .attr('x2', d => xScale(d)) + .attr('y1', 0) + .attr('y2', innerHeight) + .attr('stroke', '#666') + .attr('stroke-width', 1) + .attr('stroke-dasharray', '4,4'); + + // Draw filled area with gradient + const area = d3.area() + .x(d => xScale(d.x)) + .y0(innerHeight) + .y1(d => yScale(d.opacity)) + .curve(d3.curveLinear); + + controlGroup.append('path') + .attr('class', 'gradient-area') + .attr('d', area(rampPoints)) + .attr('fill', `url(#${gradientId})`) + .attr('opacity', 0.85); + + // Draw ramp line + const line = d3.line() + .x(d => xScale(d.x)) + .y(d => yScale(d.opacity)) + .curve(d3.curveLinear); + + // Draw ramp handles + controlGroup.selectAll('.ramp-handle') + .data(internalRampRange) + .enter() + .append('rect') + .attr('class', 'ramp-handle') + .attr('x', d => xScale(d) - 6) + .attr('y', (_, i) => yScale(i) - 6) + .attr('width', 12) + .attr('height', 12) + .attr('fill', '#fff') + .attr('stroke', `rgb(${channelColor.join(',')})`) + .attr('stroke-width', 2); + } + }, [controlPoints, internalRampRange, useAdvancedMode, xScale, yScale, channelColor, channelIndex, createGradientDef, innerHeight, innerWidth]); + + // Update LUT + const updateLut = useCallback(() => { + const lut = new Lut(); + if (useAdvancedMode) { + lut.createFromControlPoints(controlPoints); + } else { + lut.createFromMinMax(internalRampRange[0], internalRampRange[1]); + } + onLutUpdate(lut, channelIndex); + }, [useAdvancedMode, controlPoints, internalRampRange, channelIndex, onLutUpdate]); + + // Mouse event handlers + const handleMouseDown = useCallback((event) => { + const point = d3.pointer(event); + const x = xScale.invert(point[0] - MARGIN.left); + const y = yScale.invert(point[1] - MARGIN.top); + + if (useAdvancedMode) { + if (y < -0.05 || y > 1.05 || x < 0 || x > 255) return; + + // Check if clicked near existing point + const existingPointIndex = controlPoints.findIndex(p => + Math.abs(xScale(p.x) - (point[0] - MARGIN.left)) < 6 && + Math.abs(yScale(p.opacity) - (point[1] - MARGIN.top)) < 6 + ); + + if (existingPointIndex >= 0) { + setSelectedPoint(existingPointIndex); + } else { + const newPoint = { + x: Math.max(0, Math.min(255, x)), + opacity: Math.max(0, Math.min(1, y)), + color: channelColor + }; + const newPoints = [...controlPoints, newPoint].sort((a, b) => a.x - b.x); + setControlPoints(newPoints); + setSelectedPoint(newPoints.findIndex(p => p === newPoint)); + onControlPointsChange?.(newPoints); + } + } else { + const pointIndex = point[0] - MARGIN.left < innerWidth / 2 ? 0 : 1; + setSelectedPoint(pointIndex); + } + setDragging(true); + }, [useAdvancedMode, controlPoints, xScale, yScale, channelColor, onControlPointsChange, innerWidth]); + + const handleMouseMove = useCallback((event) => { + if (!dragging || selectedPoint === null) return; + + const point = d3.pointer(event); + const x = Math.min(255, Math.max(0, xScale.invert(point[0] - MARGIN.left))); + const y = Math.min(1, Math.max(0, yScale.invert(point[1] - MARGIN.top))); + + if (useAdvancedMode) { + const newPoints = [...controlPoints]; + newPoints[selectedPoint] = { + ...newPoints[selectedPoint], + x, + opacity: y + }; + const sortedPoints = newPoints.sort((a, b) => a.x - b.x); + setControlPoints(sortedPoints); + onControlPointsChange?.(sortedPoints); + updateLut(); + } else { + const newRange = [...internalRampRange]; + newRange[selectedPoint] = x; + if (selectedPoint === 0) { + newRange[0] = Math.min(newRange[0], newRange[1] - 1); + } else { + newRange[1] = Math.max(newRange[1], newRange[0] + 1); + } + setInternalRampRange(newRange); + onRampRangeChange?.(newRange); + updateLut(); + } + }, [dragging, selectedPoint, xScale, yScale, useAdvancedMode, controlPoints, internalRampRange, onControlPointsChange, onRampRangeChange, updateLut]); + + const handleMouseUp = useCallback(() => { + if (dragging) { + updateLut(); + } + setDragging(false); + setSelectedPoint(null); + }, [dragging, updateLut]); + + // Initialize and set up event listeners + useEffect(() => { + if (!svgRef.current) return; + + const svg = d3.select(svgRef.current); + svg.selectAll('*').remove(); + + // Title + svg.append('text') + .attr('class', 'graph-title') + .attr('x', width / 2) + .attr('y', MARGIN.top / 2) + .attr('text-anchor', 'middle') + .attr('dominant-baseline', 'middle') + .style('font-size', '14px') + .style('font-weight', '500') + .text('Intensity-to-Visibility Mapping'); + + // Create main container groups + const container = svg.append('g') + .attr('transform', `translate(${MARGIN.left},${MARGIN.top})`); + + container.append('g') + .attr('class', 'histogram-group'); + + container.append('g') + .attr('class', 'control-points-group'); + + // Add axes + const xAxis = d3.axisBottom(xScale) + .tickValues([0, 50, 100, 150, 200, 255]) + .tickFormat(d3.format('d')); + + const yAxis = d3.axisLeft(yScale) + .ticks(5) + .tickFormat(d3.format('.2f')); + + container.append('g') + .attr('class', 'x-axis') + .attr('transform', `translate(0,${innerHeight})`) + .call(xAxis); + + container.append('g') + .attr('class', 'y-axis') + .call(yAxis); + + + // Draw initial state + drawHistogram(); + updateVisualization(); + + // Event listeners + const svgElement = svg.node(); + svgElement.addEventListener('mousedown', handleMouseDown); + svgElement.addEventListener('mousemove', handleMouseMove); + svgElement.addEventListener('mouseup', handleMouseUp); + svgElement.addEventListener('mouseleave', handleMouseUp); + + return () => { + svgElement.removeEventListener('mousedown', handleMouseDown); + svgElement.removeEventListener('mousemove', handleMouseMove); + svgElement.removeEventListener('mouseup', handleMouseUp); + svgElement.removeEventListener('mouseleave', handleMouseUp); + }; + }, [width, height, innerHeight, innerWidth, xScale, yScale, drawHistogram, updateVisualization, handleMouseDown, handleMouseMove, handleMouseUp]); + + return ( +
+ + + {!useAdvancedMode && ( +
+
+ + { + const newValue = Math.min(Number(e.target.value), internalRampRange[1]); + const newRange = [newValue, internalRampRange[1]]; + setInternalRampRange(newRange); + onRampRangeChange?.(newRange); + updateLut(); + }} + /> +
+
+ + { + const newValue = Math.max(Number(e.target.value), internalRampRange[0]); + const newRange = [internalRampRange[0], newValue]; + setInternalRampRange(newRange); + onRampRangeChange?.(newRange); + updateLut(); + }} + /> +
+
+ )} + + +
+ ); +}; + +export default TransferFunctionEditor; diff --git a/src/components/VolumeViewer.js b/src/components/VolumeViewer.js new file mode 100644 index 0000000..f01d13b --- /dev/null +++ b/src/components/VolumeViewer.js @@ -0,0 +1,2444 @@ +import React, { useEffect, useRef, useState, useCallback } from "react"; +import { + LoadSpec, + View3d, + VolumeFileFormat, + RENDERMODE_PATHTRACE, + RENDERMODE_RAYMARCH, + VolumeMaker, + Light, + AREA_LIGHT, + SKY_LIGHT, + Lut, +} from "@aics/volume-viewer"; +import * as THREE from "three"; +import { + loaderContext, + PREFETCH_DISTANCE, + MAX_PREFETCH_CHUNKS, + myState, +} from "./appConfig"; +import { useConstructor } from "./useConstructor"; +import { + Layout, + Tabs, + Collapse, + Switch, + Slider, + InputNumber, + Row, + Col, + Button, + Select, + Input, + Spin, + Tooltip, +} from "antd"; +import { + Settings, + Files, + Info, + Sun, + Camera, + Eye, + Sliders, + Box, + Move3d, + Palette, + Scissors, + Maximize2, + Image, + Lightbulb, + Wand2, +} from "lucide-react"; +import axios from "axios"; +import { API_URL } from "../config"; // Importing API_URL from your config +import { + ALPHA_MASK_SLIDER_3D_DEFAULT, + BRIGHTNESS_SLIDER_LEVEL_DEFAULT, + CELL_SEGMENTATION_CHANNEL_NAME, + DENSITY_SLIDER_LEVEL_DEFAULT, + ISOSURFACE_OPACITY_SLIDER_MAX, + LEVELS_SLIDER_DEFAULT, + LUT_MAX_PERCENTILE, + LUT_MIN_PERCENTILE, + PRESET_COLORS_0, + PRESET_COLOR_MAP, + VIEWER_3D_SETTING, +} from "./constants"; +import PlanarSlicePlayer from "./PlanarSlicePlayer"; +import FilesList from "./FilesList"; +import ThreePointGammaSlider from "./ThreePointGammaSlider"; +import ClipRegionSlider from "./ClipRegionSlider"; +import TransferFunctionEditor from "./TransferFunctionEditor"; +// Utility function to concatenate arrays +const concatenateArrays = (arrays) => { + const totalLength = arrays.reduce((acc, arr) => acc + arr.length, 0); + const result = new Uint8Array(totalLength); + let offset = 0; + for (const arr of arrays) { + result.set(arr, offset); + offset += arr.length; + } + return result; +}; + +const { Sider, Content } = Layout; +const { TabPane } = Tabs; +const { Vector3 } = THREE; + +const VolumeViewer = () => { + const viewerRef = useRef(null); + const volumeRef = useRef(null); + const view3D = useConstructor( + () => new View3d({ parentElement: viewerRef.current }), + ); + const loadContext = useConstructor(() => loaderContext); + + const [loader, setLoader] = useState(null); + const [fileData, setFileData] = useState({}); + const [selectedBodyPart, setSelectedBodyPart] = useState(null); + const [selectedFile, setSelectedFile] = useState(null); + const [currentVolume, setCurrentVolume] = useState(null); + const [density, setDensity] = useState(myState.density); + const [exposure, setExposure] = useState(myState.exposure); + const [lights, setLights] = useState([ + new Light(SKY_LIGHT), + new Light(AREA_LIGHT), + ]); + const [isPT, setIsPT] = useState(myState.isPT); + const [channels, setChannels] = useState([]); + const [cameraMode, setCameraMode] = useState("3D"); + const [isTurntable, setIsTurntable] = useState(false); + const [showAxis, setShowAxis] = useState(false); + const [showBoundingBox, setShowBoundingBox] = useState(false); + const [showScaleBar, setShowScaleBar] = useState(true); + const [backgroundColor, setBackgroundColor] = useState( + myState.backgroundColor, + ); + const [boundingBoxColor, setBoundingBoxColor] = useState( + myState.boundingBoxColor, + ); + const [flipX, setFlipX] = useState(1); + const [flipY, setFlipY] = useState(1); + const [flipZ, setFlipZ] = useState(1); + const [gamma, setGamma] = useState([0, 0.5, 1]); + const [clipRegion, setClipRegion] = useState({ + xmin: myState.xmin, + xmax: myState.xmax, + ymin: myState.ymin, + ymax: myState.ymax, + zmin: myState.zmin, + zmax: myState.zmax, + }); + const [isPlaying, setIsPlaying] = useState(false); + const [currentFrame, setCurrentFrame] = useState(0); + const [totalFrames, setTotalFrames] = useState(0); + const [timerId, setTimerId] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const [maskAlpha, setMaskAlpha] = useState(myState.maskAlpha); + const [primaryRay, setPrimaryRay] = useState(myState.primaryRay); + const [secondaryRay, setSecondaryRay] = useState(myState.secondaryRay); + const [fov, setFov] = useState(myState.fov); + const [focalDistance, setFocalDistance] = useState(myState.focal_distance); + const [aperture, setAperture] = useState(myState.aperture); + const [samplingRate, setSamplingRate] = useState(myState.samplingRate); + + const [skyTopIntensity, setSkyTopIntensity] = useState( + myState.skyTopIntensity, + ); + const [skyMidIntensity, setSkyMidIntensity] = useState( + myState.skyMidIntensity, + ); + const [skyBotIntensity, setSkyBotIntensity] = useState( + myState.skyBotIntensity, + ); + const [skyTopColor, setSkyTopColor] = useState(myState.skyTopColor); + const [skyMidColor, setSkyMidColor] = useState(myState.skyMidColor); + const [skyBotColor, setSkyBotColor] = useState(myState.skyBotColor); + const [lightColor, setLightColor] = useState(myState.lightColor); + const [lightIntensity, setLightIntensity] = useState(myState.lightIntensity); + const [lightTheta, setLightTheta] = useState(myState.lightTheta); + const [lightPhi, setLightPhi] = useState(myState.lightPhi); + const [currentPreset, setCurrentPreset] = useState(0); // Default preset + const [settings, setSettings] = useState({ + maskAlpha: ALPHA_MASK_SLIDER_3D_DEFAULT[0], // 50 + brightness: BRIGHTNESS_SLIDER_LEVEL_DEFAULT[0], // 70 + density: DENSITY_SLIDER_LEVEL_DEFAULT[0], // 50 + levels: LEVELS_SLIDER_DEFAULT, // [35.0, 140.0, 255.0] + autoRotate: false, + pathTrace: false, + renderMode: RENDERMODE_RAYMARCH, + colorizeEnabled: false, + colorizeAlpha: 1.0, + selectedColorPalette: 0, + axisClip: { x: [0, 1], y: [0, 1], z: [0, 1] }, + }); + + // Add new state for persisting view settings + const [persistentSettings, setPersistentSettings] = useState({ + mode: "3D", // Default to 3D mode + channelSettings: {}, // Store channel-specific settings + density: 50, + brightness: 70, + maskAlpha: 50, + primaryRay: 1, + secondaryRay: 1, + clipRegion: { + xmin: 0, + xmax: 1, + ymin: 0, + ymax: 1, + zmin: 0, + zmax: 1, + }, + }); + + const [isoSurfaceSettings, setIsoSurfaceSettings] = useState({ + isosurfaceOpacityMax: ISOSURFACE_OPACITY_SLIDER_MAX, + defaultIsovalue: 128, + defaultOpacity: 1.0, + }); + const [hasScrolledOnce, setHasScrolledOnce] = useState(false); + const [originalScrollPosition, setOriginalScrollPosition] = useState(0); + + const densitySliderToView3D = (density) => density / 50.0; + + const onChannelDataArrived = (volume, channelIndex) => { + if (volume !== volumeRef.current) return; + + const histogram = volume.getHistogram(channelIndex); + if (!histogram) return; + + // Find percentile values + const hmin = histogram.findBinOfPercentile(LUT_MIN_PERCENTILE); + const hmax = histogram.findBinOfPercentile(LUT_MAX_PERCENTILE); + + // Create LUT using the Lut class + const lut = new Lut(); + const lutData = lut.createFromMinMax(hmin, hmax); + + // Set the LUT for the channel + volume.setLut(channelIndex, lutData); + + view3D.onVolumeData(volume, [channelIndex]); + + if (channels[channelIndex]) { + view3D.setVolumeChannelEnabled( + volume, + channelIndex, + channels[channelIndex].enabled, + ); + view3D.setVolumeChannelOptions(volume, channelIndex, { + color: channels[channelIndex].color, + opacity: 1.0, + brightness: 1.2, + contrast: 1.1, + }); + } + + view3D.updateActiveChannels(volume); + view3D.updateLuts(volume); + + if (volume.isLoaded()) { + // console.log("Volume " + volume.name + " is loaded"); + } + view3D.redraw(); + }; + + // Modify your onVolumeCreated function + const onVolumeCreated = (volume) => { + if (!volume || !volume.imageInfo) { + // console.error("Invalid volume data"); + return; + } + + // console.log("Volume created with info:", volume.imageInfo); + + volumeRef.current = volume; + view3D.removeAllVolumes(); + + // Log the dimensions specifically + // console.log("Dimensions:", { + // sizeX: volume.imageInfo.sizeX, + // sizeY: volume.imageInfo.sizeY, + // sizeZ: volume.imageInfo.sizeZ, + // sizeC: volume.imageInfo.sizeC, + // }); + + // Initialize channels with persisted settings if available + const channelNames = volume.imageInfo.channelNames || []; + const newChannels = channelNames.map((name, index) => { + const persistedChannel = persistentSettings.channelSettings[index] || {}; + const defaultColor = PRESET_COLORS_0[index % PRESET_COLORS_0.length]; + + return { + name, + enabled: persistedChannel.enabled ?? index < 3, + color: persistedChannel.color || defaultColor, + isosurfaceEnabled: persistedChannel.isosurfaceEnabled ?? false, + isovalue: persistedChannel.isovalue ?? 128, + opacity: persistedChannel.opacity ?? 1.0, + lut: ["p50", "p98"], + }; + }); + + // Add volume with persisted settings + view3D.addVolume(volume, { + channels: newChannels.map((ch) => ({ + enabled: ch.enabled, + color: ch.color, + isosurfaceEnabled: ch.isosurfaceEnabled, + isovalue: ch.isovalue, + isosurfaceOpacity: ch.opacity, + })), + }); + + // Apply persisted view mode and settings + setCameraMode(persistentSettings.mode); + view3D.setCameraMode(persistentSettings.mode); + + // Apply other persisted settings + updateSetting("density", persistentSettings.density); + updateSetting("brightness", persistentSettings.brightness); + updateSetting("maskAlpha", persistentSettings.maskAlpha); + setPrimaryRay(persistentSettings.primaryRay); + setSecondaryRay(persistentSettings.secondaryRay); + setClipRegion(persistentSettings.clipRegion); + + // 4. Apply initial volume settings + // Mask alpha + const alphaValue = 1 - settings.maskAlpha / 100; + view3D.updateMaskAlpha(volume, alphaValue); + + // Brightness + const brightnessValue = settings.brightness / 100; + view3D.updateExposure(brightnessValue); + + // Density + const densityValue = settings.density / 100; + view3D.updateDensity(volume, densityValue); + + // Gamma levels + const [min, mid, max] = settings.levels.map((v) => v / 255); + const diff = max - min; + const x = (mid - min) / diff; + const scale = 4 * x * x; + view3D.setGamma(volume, min, scale, max); + + channelNames.forEach((_, index) => { + if (volume.getHistogram) { + const histogram = volume.getHistogram(index); + if (histogram) { + // Find percentile values + const hmin = histogram.findBinOfPercentile(LUT_MIN_PERCENTILE); + const hmax = histogram.findBinOfPercentile(LUT_MAX_PERCENTILE); + + // Create LUT using the Lut class + const lut = new Lut(); + const lutData = lut.createFromMinMax(hmin, hmax); + + // Set the LUT for the channel + volume.setLut(index, lutData); + + // Save control points if needed + const controlPoints = [ + { x: 0, opacity: 0, color: newChannels[index].color }, + { x: hmin, opacity: 0.1, color: newChannels[index].color }, + { + x: (hmin + hmax) / 2, + opacity: 0.5, + color: newChannels[index].color, + }, + { x: hmax, opacity: 1.0, color: newChannels[index].color }, + { x: 255, opacity: 1.0, color: newChannels[index].color }, + ]; + + newChannels[index].controlPoints = controlPoints; + } + } + }); + + // Initialize masks and LUTs + const segIndex = channelNames.findIndex( + (name) => name === CELL_SEGMENTATION_CHANNEL_NAME, + ); + if (segIndex !== -1) { + view3D.setVolumeChannelAsMask(volume, segIndex); + } + + view3D.setVolumeRenderMode( + settings.pathTrace ? RENDERMODE_PATHTRACE : RENDERMODE_RAYMARCH, + ); + view3D.updateActiveChannels(volume); + view3D.updateLuts(volume); + + setChannels(newChannels); + setCurrentVolume(volume); + view3D.redraw(); + }; + + const loadVolume = async (loadSpec, loader) => { + const volume = await loader.createVolume(loadSpec, onChannelDataArrived); + // console.log("Loaded volume metadata:", volume.imageInfo); + onVolumeCreated(volume); + + // console.log(volume.imageInfo, volume.imageInfo.times); + // Set total frames based on the volume's metadata (assuming 'times' represents the number of frames) + setTotalFrames(volume.imageInfo.times || 1); + await loader.loadVolumeData(volume); + }; + + const loadVolumeFromServer = async (url) => { + setIsLoading(true); + try { + const loadSpec = new LoadSpec(); + const isZarr = url.endsWith(".zarr"); + const volumeFileType = isZarr + ? VolumeFileFormat.ZARR + : url.match(/\.(tiff?|ome\.tiff?)$/i) + ? VolumeFileFormat.TIFF + : null; + + if (!volumeFileType) { + throw new Error("Unsupported file format"); + } + + const loader = await loadContext.createLoader(url, { + fileType: volumeFileType, + fetchOptions: { + maxPrefetchDistance: PREFETCH_DISTANCE, + maxPrefetchChunks: MAX_PREFETCH_CHUNKS, + }, + }); + + setLoader(loader); + await loadVolume(loadSpec, loader); + } catch (error) { + // console.error("Error loading volume:", error); + // You might want to show an error message to the user here + } finally { + setIsLoading(false); + } + }; + + const createTestVolume = () => { + const sizeX = 64; + const sizeY = 64; + const sizeZ = 64; + const imgData = { + name: "AICS-10_5_5", + sizeX, + sizeY, + sizeZ, + sizeC: 3, + physicalPixelSize: [1, 1, 1], + spatialUnit: "", + channelNames: ["DRAQ5", "EGFP", "SEG_Memb"], + }; + + const channelVolumes = [ + VolumeMaker.createSphere(sizeX, sizeY, sizeZ, 24), + VolumeMaker.createTorus(sizeX, sizeY, sizeZ, 24, 8), + VolumeMaker.createCone(sizeX, sizeY, sizeZ, 24, 24), + ]; + + const alldata = concatenateArrays(channelVolumes); + return { + metadata: imgData, + data: { + dtype: "uint8", + shape: [channelVolumes.length, sizeZ, sizeY, sizeX], + buffer: new DataView(alldata.buffer), + }, + }; + }; + + const fetchFiles = async () => { + try { + const response = await axios.get(`${API_URL}/files`); + setFileData(response.data); + } catch (error) { + console.error("Error fetching files:", error); + } + }; + + const handleFileSelect = async (category, fileName) => { + setSelectedBodyPart(category); + setSelectedFile(fileName); + + const fileUrl = `${API_URL}/${category}/${fileName}`; + // console.log("Loading file:", fileUrl); + + await loadVolumeFromServer(fileUrl); + }; + + useEffect(() => { + fetchFiles(); + }, []); + + useEffect(() => { + if (viewerRef.current) { + const container = viewerRef.current; + container.appendChild(view3D.getDOMElement()); + + const handleResize = () => view3D.resize(); + window.addEventListener("resize", handleResize); + + view3D.resize(); + + return () => { + window.removeEventListener("resize", handleResize); + if (view3D.getDOMElement().parentNode) { + view3D.getDOMElement().parentNode.removeChild(view3D.getDOMElement()); + } + view3D.removeAllVolumes(); + }; + } + }, [viewerRef, view3D]); + + useEffect(() => { + if (!currentVolume || !view3D) return; + const densityValue = settings.density / 100; + view3D.updateDensity(currentVolume, densityValue); + view3D.redraw(); + }, [settings.density]); + + useEffect(() => { + if (!view3D) return; + const brightnessValue = settings.brightness / 100; + view3D.updateExposure(brightnessValue); + view3D.redraw(); + }, [settings.brightness]); + + useEffect(() => { + view3D.setVolumeRenderMode( + isPT ? RENDERMODE_PATHTRACE : RENDERMODE_RAYMARCH, + ); + view3D.redraw(); + }, [isPT, view3D]); + + useEffect(() => { + if (currentVolume) { + view3D.updateLights(lights); + view3D.updateActiveChannels(currentVolume); + view3D.redraw(); + } + }, [lights, view3D]); + + useEffect(() => { + if (currentVolume) { + view3D.updateActiveChannels(currentVolume); + view3D.updateLuts(currentVolume); + view3D.redraw(); + } + }, [channels]); + + useEffect(() => { + view3D.setCameraMode(cameraMode); + }, [cameraMode]); + + useEffect(() => { + view3D.setAutoRotate(isTurntable); + }, [isTurntable, view3D]); + + useEffect(() => { + view3D.setShowAxis(showAxis); + }, [showAxis, view3D]); + + useEffect(() => { + if (currentVolume) { + view3D.setShowBoundingBox(currentVolume, showBoundingBox); + } + }, [currentVolume, showBoundingBox, view3D]); + + useEffect(() => { + view3D.setShowScaleBar(showScaleBar); + }, [showScaleBar, view3D]); + + useEffect(() => { + view3D.setBackgroundColor(backgroundColor); + }, [backgroundColor, view3D]); + + useEffect(() => { + if (currentVolume) { + view3D.setBoundingBoxColor(currentVolume, boundingBoxColor); + } + }, [boundingBoxColor]); + + useEffect(() => { + if (currentVolume) { + view3D.setFlipVolume(currentVolume, flipX, flipY, flipZ); + } + }, [flipX, flipY, flipZ]); + + useEffect(() => { + if (!currentVolume || !view3D) return; + const [min, mid, max] = settings.levels.map((v) => v / 255); + const diff = max - min; + const x = (mid - min) / diff; + const scale = 4 * x * x; + view3D.setGamma(currentVolume, min, scale, max); + view3D.redraw(); + }, [settings.levels]); + + useEffect(() => { + if (currentVolume) { + view3D.updateClipRegion( + currentVolume, + clipRegion.xmin, + clipRegion.xmax, + clipRegion.ymin, + clipRegion.ymax, + clipRegion.zmin, + clipRegion.zmax, + ); + } + }, [clipRegion]); + + useEffect(() => { + if (currentVolume) { + view3D.updateCamera(fov, focalDistance, aperture); + view3D.updateActiveChannels(currentVolume); + view3D.redraw(); + } + }, [fov, focalDistance, aperture]); + + useEffect(() => { + if (currentVolume) { + view3D.setRayStepSizes(currentVolume, primaryRay, secondaryRay); + view3D.updateActiveChannels(currentVolume); + view3D.redraw(); + } + }, [primaryRay, secondaryRay]); + + useEffect(() => { + if (!currentVolume || !view3D) return; + const alphaValue = 1 - settings.maskAlpha / 100.0; + view3D.updateMaskAlpha(currentVolume, alphaValue); + view3D.updateActiveChannels(currentVolume); + // view3D.redraw(); + // console.log("maskAlpha", settings.maskAlpha); + }, [settings.maskAlpha]); + + useEffect(() => { + if (view3D && lights[0]) { + const skyLight = lights[0]; + skyLight.mColorTop = new Vector3( + (skyTopColor[0] / 255.0) * skyTopIntensity, + (skyTopColor[1] / 255.0) * skyTopIntensity, + (skyTopColor[2] / 255.0) * skyTopIntensity, + ); + skyLight.mColorMiddle = new Vector3( + (skyMidColor[0] / 255.0) * skyMidIntensity, + (skyMidColor[1] / 255.0) * skyMidIntensity, + (skyMidColor[2] / 255.0) * skyMidIntensity, + ); + skyLight.mColorBottom = new Vector3( + (skyBotColor[0] / 255.0) * skyBotIntensity, + (skyBotColor[1] / 255.0) * skyBotIntensity, + (skyBotColor[2] / 255.0) * skyBotIntensity, + ); + view3D.updateLights(lights); + view3D.updateActiveChannels(currentVolume); + view3D.redraw(); + // console.log([ + // skyTopColor, + // skyTopIntensity, + // skyMidColor, + // skyMidIntensity, + // skyBotColor, + // skyBotIntensity, + // ]); + } + }, [ + skyTopColor, + skyTopIntensity, + skyMidColor, + skyMidIntensity, + skyBotColor, + skyBotIntensity, + ]); + + // useEffect for area light + useEffect(() => { + if (view3D && lights[1]) { + const areaLight = lights[1]; + areaLight.mColor = new Vector3( + (lightColor[0] / 255.0) * lightIntensity, + (lightColor[1] / 255.0) * lightIntensity, + (lightColor[2] / 255.0) * lightIntensity, + ); + areaLight.mTheta = (lightTheta * Math.PI) / 180.0; + areaLight.mPhi = (lightPhi * Math.PI) / 180.0; + view3D.updateLights(lights); + view3D.updateActiveChannels(currentVolume); + view3D.redraw(); + } + // console.log([lightColor, lightIntensity, lightTheta, lightPhi]); + }, [lightColor, lightIntensity, lightTheta, lightPhi]); + + // Effect for handling isosurface enable/disable + useEffect(() => { + if (!currentVolume || !view3D) return; + + channels.forEach((channel, index) => { + view3D.setVolumeChannelOptions(currentVolume, index, { + isosurfaceEnabled: channel.isosurfaceEnabled, + isovalue: channel.isovalue, + opacity: channel.opacity, + }); + }); + + view3D.updateMaterial(currentVolume); + view3D.redraw(); + }, [channels.map((ch) => ch.isosurfaceEnabled).join(",")]); // Dependency on isosurfaceEnabled values + + // Effect for handling isovalue changes + useEffect(() => { + if (!currentVolume || !view3D) return; + + channels.forEach((channel, index) => { + if (channel.isosurfaceEnabled) { + view3D.setVolumeChannelOptions(currentVolume, index, { + isosurfaceEnabled: true, + isovalue: channel.isovalue, + opacity: channel.opacity, + }); + } + }); + + view3D.updateMaterial(currentVolume); + view3D.redraw(); + }, [channels.map((ch) => ch.isovalue).join(",")]); // Dependency on isovalue changes + + // Effect for handling opacity changes + useEffect(() => { + if (!currentVolume || !view3D) return; + + channels.forEach((channel, index) => { + if (channel.isosurfaceEnabled) { + view3D.setVolumeChannelOptions(currentVolume, index, { + isosurfaceEnabled: true, + isovalue: channel.isovalue, + isosurfaceOpacity: channel.opacity, + }); + } + }); + + view3D.updateMaterial(currentVolume); + view3D.redraw(); + }, [channels.map((ch) => ch.opacity).join(",")]); // Dependency on opacity changes + + useEffect(() => { + if (!currentVolume || !view3D) return; + channels.forEach((channel, index) => { + if (channel.isosurfaceEnabled) { + view3D.setVolumeChannelOptions(currentVolume, index, { + isosurfaceEnabled: true, + isovalue: channel.isovalue, + isosurfaceOpacity: channel.opacity, + opacity: channel.opacity, // Include both for compatibility + }); + // Force material update + view3D.updateMaterial(currentVolume); + } + }); + view3D.redraw(); + }, [channels.map((ch) => `${ch.isosurfaceEnabled}-${ch.opacity}`).join(",")]); + + const setInitialRenderMode = () => { + view3D.setVolumeRenderMode( + isPT ? RENDERMODE_PATHTRACE : RENDERMODE_RAYMARCH, + ); + view3D.setMaxProjectMode(currentVolume, false); + }; + + const DEFAULT_CHANNEL_COLOR = [128, 128, 128]; // Medium gray + + // Modify your showChannelUI function + const showChannelUI = (volume) => { + const currentPresetColors = PRESET_COLOR_MAP[currentPreset].colors; + + const channelGui = volume.imageInfo.channelNames.map((name, index) => { + const channelColor = + currentPresetColors[index % currentPresetColors.length]; + + return { + name, + enabled: index < 3, + colorD: channelColor, + colorS: [0, 0, 0], + colorE: [0, 0, 0], + glossiness: 0, + window: 1, + level: 0.5, + isovalue: 128, + isosurface: false, + brightness: 1.2, + contrast: 1.1, + // Add these new properties + useAdvancedMode: false, + controlPoints: [ + { x: 0, opacity: 0, color: channelColor }, + { x: 255, opacity: 1, color: channelColor } + ] + }; + }); + + setChannels(channelGui); + + // Force update channel materials + channelGui.forEach((channel, index) => { + if (channel.enabled) { + view3D.updateChannelMaterial( + volume, + index, + channel.colorD, + channel.colorS, + channel.colorE, + channel.glossiness, + ); + } + }); + + view3D.updateMaterial(volume); + view3D.redraw(); + }; + + const updateChannel = (index, key, value) => { + const updatedChannels = [...channels]; + updatedChannels[index][key] = value; + setChannels(updatedChannels); + + if (currentVolume) { + if (key === "enabled") { + view3D.setVolumeChannelEnabled(currentVolume, index, value); + } else if (key === "isosurface") { + view3D.setVolumeChannelOptions(currentVolume, index, { + isosurfaceEnabled: value, + }); + if (value) { + view3D.createIsosurface( + currentVolume, + index, + updatedChannels[index].isovalue, + 1.0, + ); + } else { + view3D.clearIsosurface(currentVolume, index); + } + } else if (["colorD", "colorS", "colorE", "glossiness"].includes(key)) { + view3D.updateChannelMaterial( + currentVolume, + index, + updatedChannels[index].colorD, + updatedChannels[index].colorS, + updatedChannels[index].colorE, + updatedChannels[index].glossiness, + ); + view3D.updateMaterial(currentVolume); + } else if (key === "window" || key === "level") { + const lut = new Lut().createFromWindowLevel( + updatedChannels[index].window, + updatedChannels[index].level, + ); + currentVolume.setLut(index, lut); + view3D.updateLuts(currentVolume); + } + view3D.redraw(); + } + }; + + const updateChannelOptions = (index, options) => { + if (!currentVolume || !view3D) return; + + const updatedChannels = [...channels]; + updatedChannels[index] = { ...updatedChannels[index], ...options }; + setChannels(updatedChannels); + }; + + const initializeChannelOptions = (volume) => { + const channelOptions = volume.imageInfo.channelNames.map((name, index) => ({ + name, + enabled: index < 3, + color: volume.channelColorsDefault[index] || [128, 128, 128], + specularColor: [0, 0, 0], + emissiveColor: [0, 0, 0], + glossiness: 0, + isosurfaceEnabled: false, + isovalue: 127, + isosurfaceOpacity: 1.0, + })); + setChannels(channelOptions); + }; + + const updateIsovalue = (index, isovalue) => { + if (!currentVolume || !view3D) return; + + const updatedChannels = [...channels]; + updatedChannels[index] = { + ...updatedChannels[index], + isovalue, + }; + setChannels(updatedChannels); + + view3D.setVolumeChannelOptions(currentVolume, index, { + isosurfaceEnabled: updatedChannels[index].isosurfaceEnabled, + isovalue: isovalue, + isosurfaceOpacity: updatedChannels[index].opacity, + }); + + view3D.updateMaterial(currentVolume); + view3D.redraw(); + }; + + // Histogram-based LUT adjustments + const updateChannelLut = (index, type) => { + if (currentVolume) { + let lut; + let newRange; + let newControlPoints; + const histogram = currentVolume.getHistogram(index); + const channelColor = channels[index].color; + + switch(type) { + case "autoIJ": { + const [hmin, hmax] = histogram.findAutoIJBins(); + lut = new Lut().createFromMinMax(hmin, hmax); + newRange = [hmin, hmax]; + // Create control points for advanced mode + newControlPoints = [ + { x: 0, opacity: 0, color: channelColor }, + { x: hmin, opacity: 0, color: channelColor }, + { x: hmax, opacity: 1, color: channelColor }, + { x: 255, opacity: 1, color: channelColor } + ]; + break; + } + case "auto0": { + const [hmin, hmax] = histogram.findAutoMinMax(); + lut = new Lut().createFromMinMax(hmin, hmax); + newRange = [hmin, hmax]; + newControlPoints = [ + { x: 0, opacity: 0, color: channelColor }, + { x: hmin, opacity: 0, color: channelColor }, + { x: hmax, opacity: 1, color: channelColor }, + { x: 255, opacity: 1, color: channelColor } + ]; + break; + } + case "bestFit": { + const [hmin, hmax] = histogram.findBestFitBins(); + lut = new Lut().createFromMinMax(hmin, hmax); + newRange = [hmin, hmax]; + newControlPoints = [ + { x: 0, opacity: 0, color: channelColor }, + { x: hmin, opacity: 0, color: channelColor }, + { x: hmax, opacity: 1, color: channelColor }, + { x: 255, opacity: 1, color: channelColor } + ]; + break; + } + case "pct50_98": { + const hmin = histogram.findBinOfPercentile(0.5); + const hmax = histogram.findBinOfPercentile(0.983); + lut = new Lut().createFromMinMax(hmin, hmax); + newRange = [hmin, hmax]; + newControlPoints = [ + { x: 0, opacity: 0, color: channelColor }, + { x: hmin, opacity: 0, color: channelColor }, + { x: hmax, opacity: 1, color: channelColor }, + { x: 255, opacity: 1, color: channelColor } + ]; + break; + } + } + + // Update channel settings + const updatedChannels = [...channels]; + updatedChannels[index] = { + ...updatedChannels[index], + controlPoints: newControlPoints, + rampRange: newRange + }; + setChannels(updatedChannels); + + // Apply to volume + currentVolume.setLut(index, lut); + view3D.updateLuts(currentVolume); + view3D.redraw(); + } + }; + + const setCameraModeHandler = (mode) => { + const previousMode = cameraMode; + setCameraMode(mode); + + if (!currentVolume || !view3D) return; + + if (mode === "3D") { + // Reset scroll flag when switching back to 3D + setHasScrolledOnce(false); + // Remove padding when returning to 3D + viewerRef.current.style.paddingBottom = "0"; + + const fullClipRegion = { + xmin: 0, + xmax: 1, + ymin: 0, + ymax: 1, + zmin: 0, + zmax: 1, + }; + + setClipRegion(fullClipRegion); + view3D.updateClipRegion( + currentVolume, + fullClipRegion.xmin, + fullClipRegion.xmax, + fullClipRegion.ymin, + fullClipRegion.ymax, + fullClipRegion.zmin, + fullClipRegion.zmax, + ); + + view3D.setCameraMode(mode); + view3D.setVolumeRenderMode( + isPT ? RENDERMODE_PATHTRACE : RENDERMODE_RAYMARCH, + ); + view3D.setMaxProjectMode(currentVolume, false); + + // Restore 3D settings + view3D.updateDensity(currentVolume, settings.density / 100); + view3D.updateExposure(settings.brightness / 100); + view3D.updateMaskAlpha(currentVolume, 1 - settings.maskAlpha / 100); + view3D.setRayStepSizes(currentVolume, primaryRay, secondaryRay); + + channels.forEach((channel, index) => { + if (currentVolume) { + view3D.setVolumeChannelEnabled(currentVolume, index, channel.enabled); + if (channel.enabled) { + view3D.setVolumeChannelOptions(currentVolume, index, { + color: channel.color, + opacity: 1.0, + brightness: 1.2, + contrast: 1.1, + }); + } + } + }); + } else { + // Add padding for the player in 2D modes + if (viewerRef.current) { + viewerRef.current.style.paddingBottom = "80px"; + } + + const defaultClipRegion = { + xmin: 0, + xmax: 1, + ymin: 0, + ymax: 1, + zmin: 0, + zmax: 1, + }; + + const sliceThickness = 0.01; + if (mode === "X") { + defaultClipRegion.xmin = 0; + defaultClipRegion.xmax = sliceThickness; + } else if (mode === "Y") { + defaultClipRegion.ymin = 0; + defaultClipRegion.ymax = sliceThickness; + } else if (mode === "Z") { + defaultClipRegion.zmin = 0; + defaultClipRegion.zmax = sliceThickness; + } + + setClipRegion(defaultClipRegion); + view3D.updateClipRegion( + currentVolume, + defaultClipRegion.xmin, + defaultClipRegion.xmax, + defaultClipRegion.ymin, + defaultClipRegion.ymax, + defaultClipRegion.zmin, + defaultClipRegion.zmax, + ); + + view3D.setCameraMode(mode); + view3D.setVolumeRenderMode(RENDERMODE_RAYMARCH); + view3D.setMaxProjectMode(currentVolume, false); + + // Set optimal 2D view settings + view3D.updateDensity(currentVolume, 0.5); + view3D.updateExposure(0.7); + view3D.updateMaskAlpha(currentVolume, 0.5); + view3D.setRayStepSizes(currentVolume, 1, 1); + + channels.forEach((channel, index) => { + if (currentVolume) { + view3D.setVolumeChannelEnabled(currentVolume, index, channel.enabled); + if (channel.enabled) { + view3D.setVolumeChannelOptions(currentVolume, index, { + color: channel.color, + opacity: 1.0, + brightness: 1.2, + contrast: 1.1, + }); + } + } + }); + } + + setPersistentSettings((prev) => ({ + ...prev, + mode: mode, + })); + + view3D.updateActiveChannels(currentVolume); + view3D.updateLuts(currentVolume); + view3D.redraw(); + }; + + const toggleTurntable = () => { + setIsTurntable(!isTurntable); + }; + + const toggleAxis = () => { + setShowAxis(!showAxis); + }; + + const toggleBoundingBox = () => { + setShowBoundingBox(!showBoundingBox); + }; + + const toggleScaleBar = () => { + setShowScaleBar(!showScaleBar); + }; + + const updateBackgroundColor = (color) => { + setBackgroundColor(color); + }; + + const updateBoundingBoxColor = (color) => { + setBoundingBoxColor(color); + }; + + const flipVolume = (axis) => { + if (axis === "X") { + setFlipX(flipX * -1); + } else if (axis === "Y") { + setFlipY(flipY * -1); + } else if (axis === "Z") { + setFlipZ(flipZ * -1); + } + }; + + const gammaSliderToImageValues = (sliderValues) => { + let min = Number(sliderValues[0]); + let mid = Number(sliderValues[1]); + let max = Number(sliderValues[2]); + if (mid > max || mid < min) { + mid = 0.5 * (min + max); + } + const div = 255; + min /= div; + max /= div; + mid /= div; + const diff = max - min; + const x = (mid - min) / diff; + let scale = 4 * x * x; + if ((mid - 0.5) * (mid - 0.5) < 0.0005) { + scale = 1.0; + } + return [min, max, scale]; + }; + + const updateGamma = (newGamma) => { + setGamma(newGamma); + }; + + const captureScreenshot = () => { + view3D.capture((dataUrl) => { + const anchor = document.createElement("a"); + anchor.href = dataUrl; + anchor.download = "screenshot.png"; + anchor.click(); + }); + }; + + const updateClipRegion = (axis, values) => { + const [min, max] = values; + const updates = {}; + + if (axis === "X") { + updates.xmin = min; + updates.xmax = max; + } else if (axis === "Y") { + updates.ymin = min; + updates.ymax = max; + } else if (axis === "Z") { + updates.zmin = min; + updates.zmax = max; + } + + const newClipRegion = { ...clipRegion, ...updates }; + setClipRegion(newClipRegion); + + if (currentVolume) { + view3D.updateClipRegion( + currentVolume, + newClipRegion.xmin, + newClipRegion.xmax, + newClipRegion.ymin, + newClipRegion.ymax, + newClipRegion.zmin, + newClipRegion.zmax, + ); + } + }; + + const goToFrame = (frame) => { + if (frame >= 0 && frame < totalFrames) { + view3D.setTime(currentVolume, frame); + setCurrentFrame(frame); + } + }; + + const goToZSlice = (slice) => { + if (currentVolume && view3D.setZSlice(currentVolume, slice)) { + // Z slice updated successfully + const zSlider = document.getElementById("zSlider"); + const zInput = document.getElementById("zValue"); + + if (zInput) { + zInput.value = slice; + } + if (zSlider) { + zSlider.value = slice; + } + } else { + console.log("Failed to update Z slice"); + } + }; + + const playTimeSeries = () => { + if (timerId) { + clearInterval(timerId); + } + setIsPlaying(true); + const newTimerId = setInterval(() => { + setCurrentFrame((prevFrame) => { + const nextFrame = (prevFrame + 1) % totalFrames; + view3D.setTime(currentVolume, nextFrame); + return nextFrame; + }); + }, 80); + setTimerId(newTimerId); + }; + + const pauseTimeSeries = () => { + if (timerId) { + clearInterval(timerId); + setTimerId(null); + } + setIsPlaying(false); + }; + + const rgbToHex = (r, g, b) => { + const toHex = (component) => { + const hex = Math.round(component).toString(16); + return hex.length === 1 ? "0" + hex : hex; + }; + + // Ensure values are between 0-255 + r = Math.min(255, Math.max(0, Math.round(r))); + g = Math.min(255, Math.max(0, Math.round(g))); + b = Math.min(255, Math.max(0, Math.round(b))); + + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; + }; + + const hexToRgb = (hex) => { + // Remove the hash if present + hex = hex.replace(/^#/, ""); + + // Parse the hex values + const bigint = parseInt(hex, 16); + const r = (bigint >> 16) & 255; + const g = (bigint >> 8) & 255; + const b = bigint & 255; + + return [r, g, b]; + }; + + const updatePixelSamplingRate = (rate) => { + setSamplingRate(rate); + view3D.updatePixelSamplingRate(rate); + view3D.redraw(); + }; + + const updateSkyLight = (position, intensity, color) => { + if (position === "top") { + setSkyTopIntensity(intensity); + setSkyTopColor(color); + } else if (position === "mid") { + setSkyMidIntensity(intensity); + setSkyMidColor(color); + } else if (position === "bot") { + setSkyBotIntensity(intensity); + setSkyBotColor(color); + } + updateLights(); + }; + + const updateAreaLight = (intensity, color, theta, phi) => { + setLightIntensity(intensity); + setLightColor(color); + setLightTheta(theta); + setLightPhi(phi); + updateLights(); + }; + + const updateLights = () => { + const updatedLights = [...lights]; + // Update sky light + updatedLights[0].mColorTop = new Vector3( + (skyTopColor[0] / 255.0) * skyTopIntensity, + (skyTopColor[1] / 255.0) * skyTopIntensity, + (skyTopColor[2] / 255.0) * skyTopIntensity, + ); + updatedLights[0].mColorMiddle = new Vector3( + (skyMidColor[0] / 255.0) * skyMidIntensity, + (skyMidColor[1] / 255.0) * skyMidIntensity, + (skyMidColor[2] / 255.0) * skyMidIntensity, + ); + updatedLights[0].mColorBottom = new Vector3( + (skyBotColor[0] / 255.0) * skyBotIntensity, + (skyBotColor[1] / 255.0) * skyBotIntensity, + (skyBotColor[2] / 255.0) * skyBotIntensity, + ); + + // Update area light + updatedLights[1].mColor = new Vector3( + (lightColor[0] / 255.0) * lightIntensity, + (lightColor[1] / 255.0) * lightIntensity, + (lightColor[2] / 255.0) * lightIntensity, + ); + updatedLights[1].mTheta = (lightTheta * Math.PI) / 180.0; + updatedLights[1].mPhi = (lightPhi * Math.PI) / 180.0; + + setLights(updatedLights); + view3D.updateLights(updatedLights); + view3D.redraw(); + }; + + const applyColorPreset = (presetIndex) => { + if (!currentVolume) return; + + const preset = PRESET_COLOR_MAP[presetIndex].colors; + + const updatedChannels = channels.map((channel, index) => { + const newColor = preset[index % preset.length]; + return { + ...channel, + colorD: newColor, + }; + }); + + // Update state + setChannels(updatedChannels); + setCurrentPreset(presetIndex); + + // Update each channel's material + updatedChannels.forEach((channel, index) => { + view3D.updateChannelMaterial( + currentVolume, + index, + channel.colorD, + channel.colorS, + channel.colorE, + channel.glossiness, + ); + }); + + view3D.updateMaterial(currentVolume); + view3D.redraw(); + }; + + const updateSetting = (key, value) => { + setSettings((prev) => ({ + ...prev, + [key]: value, + })); + }; + + const handleClipRegionUpdate = (newClipRegion) => { + setClipRegion(newClipRegion); + + if (currentVolume) { + view3D.updateClipRegion( + currentVolume, + newClipRegion.xmin, + newClipRegion.xmax, + newClipRegion.ymin, + newClipRegion.ymax, + newClipRegion.zmin, + newClipRegion.zmax, + ); + } + }; + + // Optional: Add handler for slice changes if you need to do something when slices change + const handleSliceChange = (newSlice) => { + // Handle slice changes if needed + console.log("Slice changed:", newSlice); + }; + + // Function to save current view settings + const saveCurrentSettings = useCallback(() => { + setPersistentSettings((prev) => ({ + ...prev, + mode: cameraMode, + density: settings.density, + brightness: settings.brightness, + maskAlpha: settings.maskAlpha, + primaryRay: primaryRay, + secondaryRay: secondaryRay, + clipRegion: clipRegion, + channelSettings: channels.reduce((acc, channel, index) => { + acc[index] = { + enabled: channel.enabled, + color: channel.color, + isosurfaceEnabled: channel.isosurfaceEnabled, + isovalue: channel.isovalue, + opacity: channel.opacity, + }; + return acc; + }, {}), + })); + }, [cameraMode, settings, primaryRay, secondaryRay, clipRegion, channels]); + + // Add effect to save settings when they change + useEffect(() => { + if (currentVolume) { + saveCurrentSettings(); + } + }, [ + cameraMode, + settings.density, + settings.brightness, + settings.maskAlpha, + primaryRay, + secondaryRay, + clipRegion, + channels, + saveCurrentSettings, + ]); + + // Update the roundToSignificantFigure function: + const roundToSignificantFigure = (value, sigFigs = 1) => { + if (value === undefined || value === null) return "N/A"; + + // Convert value to number if it's a string + const numericValue = Number(value); + + // Return NaN if conversion failed + if (isNaN(numericValue)) return "N/A"; + + // Return 0 if the value is 0 + if (numericValue === 0) return "0µm"; + + // Calculate scale for rounding + const scale = Math.pow( + 10, + Math.floor(Math.log10(Math.abs(numericValue))) + 1 - sigFigs, + ); + + // Perform rounding + const roundedValue = Math.round(numericValue / scale) * scale; + + return `${roundedValue}`; + }; + // console.log(currentVolume?.imageMetadata); + + const getDimensionOrder = (metadata) => { + let order = []; + if (metadata["Time series frames"] > 1) order.push("T"); + if (metadata.Channels > 1) order.push("C"); + order.push("Z", "Y", "X"); + return order.join(""); + }; + + return ( + +
+ + + {/* Files Tab */} + + Files + + } + key="files" + > + + handleFileSelect(category, file.name) + } + /> + + {/* Settings Tab */} + + Settings + + } + key="settings" + > + + {/* Channels */} + + Channels + + } + key="channels" + > + + Color Preset + + + + + {channels.map((channel, index) => ( +
+
+ {channel.name || `Channel ${index + 1}`} +
+ {channel.enabled && ( +
+ { + if (currentVolume) { + currentVolume.setLut(channelIndex, lut); + view3D.updateLuts(currentVolume); + view3D.redraw(); + } + }} + useAdvancedMode={channel.useAdvancedMode} + initialControlPoints={channel.controlPoints} + rampRange={channel.rampRange} + channelColor={channel.color} + onRampRangeChange={(newRange) => { + const updatedChannels = [...channels]; + updatedChannels[index].rampRange = newRange; + setChannels(updatedChannels); + }} + onControlPointsChange={(newPoints) => { + const updatedChannels = [...channels]; + updatedChannels[index].controlPoints = newPoints; + setChannels(updatedChannels); + }} + /> + {/* Advanced Mode Toggle with improved styling */} +
+ { + const updatedChannels = [...channels]; + updatedChannels[index].useAdvancedMode = checked; + setChannels(updatedChannels); + }} + size="small" + /> + Advanced Mode +
+
+ )} + + Enable Channel + + + updateChannelOptions(index, { enabled }) + } + /> + + + + Enable Isosurface + + + updateChannelOptions(index, { + isosurfaceEnabled: enabled, + }) + } + /> + + + {channel.isosurfaceEnabled && ( + <> + + Isovalue + + + updateChannelOptions(index, { + isovalue: value, + }) + } + /> + + + + Opacity + + + updateChannelOptions(index, { + opacity: value / 100, + }) + } + /> + + + + )} + + Color + + { + const newColor = hexToRgb(e.target.value); + updateChannelOptions(index, { color: newColor }); + if (currentVolume && view3D) { + view3D.updateChannelMaterial( + currentVolume, + index, + newColor, + [0, 0, 0], + [0, 0, 0], + 0, + ); + view3D.updateMaterial(currentVolume); + view3D.redraw(); + } + }} + /> + + + + +
+
+ + + +
+
+ + +
+
+ +
+
+ ))} +
+ + {/* Gamma */} + + Gamma + + } + key="gamma" + > + updateSetting("levels", values)} + /> + + + {/* Exposure */} + + Exposure + + } + key="exposure" + > + updateSetting("brightness", val)} + /> + + + {/* Camera Mode */} + + Camera Mode + + } + key="cameraMode" + > + + + + {/* View Controls */} + + View Controls + + } + key="controls" + > + + + + + + Background Color + + { + const [r, g, b] = hexToRgb(e.target.value); + const normalizedColor = [r / 255, g / 255, b / 255]; + setBackgroundColor(normalizedColor); + if (view3D) { + view3D.setBackgroundColor(normalizedColor); + view3D.redraw(); + } + }} + /> + + + + Bounding Box Color + + { + const [r, g, b] = hexToRgb(e.target.value); + const normalizedColor = [r / 255, g / 255, b / 255]; + setBoundingBoxColor(normalizedColor); + if (currentVolume && view3D) { + view3D.setBoundingBoxColor( + currentVolume, + normalizedColor, + ); + view3D.redraw(); + } + }} + /> + + + + + + + + {/* Clip Region */} + + Clip Region + + } + key="clipRegion" + > +
+ {currentVolume && ( + <> + updateClipRegion("X", values)} + totalSlices={currentVolume.imageMetadata.Dimensions.x} + /> + updateClipRegion("Y", values)} + totalSlices={currentVolume.imageMetadata.Dimensions.y} + /> + updateClipRegion("Z", values)} + totalSlices={currentVolume.imageMetadata.Dimensions.z} + /> + + )} +
+
+ + {/* Camera Settings */} + + Camera Settings + + } + key="camera" + > + + FOV + + + + + + Focal Distance + + + + + + Aperture + + + + + + + {/* Render Mode */} + + Render Mode + + } + key="renderMode" + > + + Path Trace + + setIsPT(checked)} + /> + + + + {isPT && ( + <> + {/* Density */} + + Density + + } + key="density" + > + updateSetting("density", val)} + /> + + + {/* Ray Steps */} + + Ray Steps + + } + key="raySteps" + > +
+ + +
+
+ + +
+
+ + {/* Sampling Rate */} + + Sampling Rate + + } + key="sampling" + > + + Pixel Sampling Rate + + + + + + + {/* Mask Alpha */} + + Mask Alpha + + } + key="maskAlpha" + > + updateSetting("maskAlpha", val)} + /> + + + )} +
+
+ + {/* Metadata Tab */} + + Info + + } + key="metadata" + > + {currentVolume && currentVolume.imageMetadata && ( +
+ +
{currentVolume.name}
+
+ +
+ Dimension order:{" "} + {(() => { + const order = []; + if (currentVolume.imageMetadata["Time series frames"] > 1) + order.push("T"); + if (currentVolume.imageMetadata.Channels > 1) + order.push("C"); + order.push("Z", "Y", "X"); + return order.join(""); + })()} +
+ + + +
+
+ x + + {currentVolume.imageMetadata.Dimensions.x} + +
+
+ y + + {currentVolume.imageMetadata.Dimensions.y} + +
+
+ z + + {currentVolume.imageMetadata.Dimensions.z} + +
+
+
+ + +
+
+ x + + { + currentVolume.imageMetadata["Original dimensions"] + .x + } + +
+
+ y + + { + currentVolume.imageMetadata["Original dimensions"] + .y + } + +
+
+ z + + { + currentVolume.imageMetadata["Original dimensions"] + .z + } + +
+
+
+ + +
+
+ x + + {currentVolume.imageMetadata["Physical size"].x} + +
+
+ y + + {currentVolume.imageMetadata["Physical size"].y} + +
+
+ z + + {currentVolume.imageMetadata["Physical size"].z} + +
+
+
+ + +
+
+ x + + { + currentVolume.imageMetadata[ + "Physical size per pixel" + ].x + } + +
+
+ y + + { + currentVolume.imageMetadata[ + "Physical size per pixel" + ].y + } + +
+
+ z + + { + currentVolume.imageMetadata[ + "Physical size per pixel" + ].z + } + +
+
+
+
+ +
+
+
+ Channels: {currentVolume.imageMetadata.Channels} +
+
+ Time series frames:{" "} + {currentVolume.imageMetadata["Time series frames"]} +
+
+ {currentVolume.imageMetadata.subresolutionLevels > 1 && ( +
+ Subresolution levels:{" "} + {currentVolume.imageMetadata.subresolutionLevels} +
+ )} +
+ + +
+ )} +
+
+
+
+ + + +
+ {/* Planar slice player */} + {cameraMode !== "3D" && currentVolume && ( + + )} +
+
+
+ + +
+ ); +}; + +export default VolumeViewer; diff --git a/src/components/WelcomePage.js b/src/components/WelcomePage.js new file mode 100755 index 0000000..8add83e --- /dev/null +++ b/src/components/WelcomePage.js @@ -0,0 +1,68 @@ +import React from "react"; +import DatasetCard from "./DatasetCard"; + +function WelcomePage() { + const gridContainerStyle = { + display: "grid", + gridTemplateColumns: "repeat(2, 1fr)", // 2x2 grid layout + gap: "20px", + marginTop: "60px", // Adjust according to navbar height + }; + + return ( +
+
+

Welcome to the ISAS LSFM Data Portal

+

+ Lightsheet fluorescence microscopy (LSFM) is a cutting-edge technique + offering high optical resolutions and superior sectioning + capabilities, ideal for whole-tissue, whole-organ, and potentially + even whole-body imaging at cellular resolution. +

+

+ This portal hosts LSFM datasets collected at ISAS and our + collaborators, shared with permission for scientific research + purposes. +

+

+ The portal offers an interactive platform for biomedical researchers + to explore these large biomedical image datasets. We aim to make these + datasets accessible to foster deeper exploration and innovative + discoveries. +

+
+ +
+

Load a dataset to get started

+
+ {/* Example DatasetCard usage */} + + + + + {/* Additional DatasetCards can be added here as more datasets become available */} +
+
+
+ ); +} + +export default WelcomePage; diff --git a/src/components/appConfig.js b/src/components/appConfig.js new file mode 100644 index 0000000..c35d006 --- /dev/null +++ b/src/components/appConfig.js @@ -0,0 +1,191 @@ +import * as THREE from 'three'; +import { + Volume, + Light, + VolumeLoaderContext, + JsonImageInfoLoader, + VolumeFileFormat, + SKY_LIGHT, + AREA_LIGHT, +} from "@aics/volume-viewer"; + +const { Vector2, Vector3 } = THREE; + +export const getDefaultImageInfo = () => ({ + name: "", + originalSize: new Vector3(1, 1, 1), + atlasTileDims: new Vector2(1, 1), + volumeSize: new Vector3(1, 1, 1), + subregionSize: new Vector3(1, 1, 1), + subregionOffset: new Vector3(0, 0, 0), + physicalPixelSize: new Vector3(1, 1, 1), + spatialUnit: "", + numChannels: 0, + channelNames: [], + channelColors: [], + times: 1, + timeScale: 1, + timeUnit: "", + numMultiscaleLevels: 1, + multiscaleLevel: 0, + transform: { + translation: new Vector3(0, 0, 0), + rotation: new Vector3(0, 0, 0), + }, +}); + +export const CACHE_MAX_SIZE = 2_000_000_000; +export const CONCURRENCY_LIMIT = 8; +export const PREFETCH_CONCURRENCY_LIMIT = 3; +export const PREFETCH_DISTANCE = [5, 5, 5, 5]; +export const MAX_PREFETCH_CHUNKS = 25; +export const PLAYBACK_INTERVAL = 80; +export const DATARANGE_UINT8 = [0, 255]; + +// Cache settings +export const QUEUE_MAX_SIZE = 10; +export const QUEUE_MAX_LOW_PRIORITY_SIZE = 4; + +export const TEST_DATA = { + timeSeries: { + type: VolumeFileFormat.JSON, + url: "https://animatedcell-test-data.s3.us-west-2.amazonaws.com/timelapse/test_parent_T49.ome_%%_atlas.json", + times: 46, + }, + omeTiff: { + type: VolumeFileFormat.TIFF, + url: "https://animatedcell-test-data.s3.us-west-2.amazonaws.com/AICS-12_881.ome.tif", + }, + zarrEMT: { + url: "https://dev-aics-dtp-001.int.allencell.org/dan-data/3500005818_20230811__20x_Timelapse-02(P27-E7).ome.zarr", + type: VolumeFileFormat.ZARR, + }, + zarrIDR1: { + type: VolumeFileFormat.ZARR, + url: "https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.4/idr0076A/10501752.zarr", + }, + zarrIDR2: { + type: VolumeFileFormat.ZARR, + url: "https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.4/idr0054A/5025553.zarr", + }, + zarrVariance: { + type: VolumeFileFormat.ZARR, + url: "https://animatedcell-test-data.s3.us-west-2.amazonaws.com/variance/1.zarr", + }, + zarrNucmorph0: { + type: VolumeFileFormat.ZARR, + url: "https://animatedcell-test-data.s3.us-west-2.amazonaws.com/20200323_F01_001/P13-C4.zarr/", + }, + zarrNucmorph1: { + type: VolumeFileFormat.ZARR, + url: "https://animatedcell-test-data.s3.us-west-2.amazonaws.com/20200323_F01_001/P15-C3.zarr/", + }, + zarrNucmorph2: { + type: VolumeFileFormat.ZARR, + url: "https://animatedcell-test-data.s3.us-west-2.amazonaws.com/20200323_F01_001/P7-B4.zarr/", + }, + zarrNucmorph3: { + type: VolumeFileFormat.ZARR, + url: "https://animatedcell-test-data.s3.us-west-2.amazonaws.com/20200323_F01_001/P8-B4.zarr/", + }, + zarrFlyBrain: { + type: VolumeFileFormat.ZARR, + url: "https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.4/idr0048A/9846152.zarr/", + }, + zarrUK: { + type: VolumeFileFormat.ZARR, + url: "https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.4/idr0062A/6001240.zarr", + }, + opencell: { type: "opencell", url: "" }, + cfeJson: { + type: VolumeFileFormat.JSON, + url: "AICS-12_881_atlas.json", + }, + abm: { + type: VolumeFileFormat.TIFF, + url: "https://animatedcell-test-data.s3.us-west-2.amazonaws.com/HAMILTONIAN_TERM_FOV_VSAHJUP_0000_000192.ome.tif", + }, + procedural: { type: VolumeFileFormat.DATA, url: "" }, +}; + +export const myState = { + file: "", + volume: new Volume(), + currentFrame: 0, + lastFrameTime: 0, + isPlaying: false, + timerId: 0, + loader: new JsonImageInfoLoader( + "https://animatedcell-test-data.s3.us-west-2.amazonaws.com/timelapse/test_parent_T49.ome_%%_atlas.json" + ), + density: 12.5, + maskAlpha: 0.0, + exposure: 0.75, + aperture: 0.0, + fov: 20, + focalDistance: 4.0, + lights: [new Light(SKY_LIGHT), new Light(AREA_LIGHT)], + skyTopIntensity: 0.3, + skyMidIntensity: 0.3, + skyBotIntensity: 0.3, + skyTopColor: [255, 255, 255], + skyMidColor: [255, 255, 255], + skyBotColor: [255, 255, 255], + lightColor: [255, 255, 255], + lightIntensity: 75.0, + lightTheta: 14, // deg + lightPhi: 54, // deg + xmin: 0.0, + ymin: 0.0, + zmin: 0.0, + xmax: 1.0, + ymax: 1.0, + zmax: 1.0, + samplingRate: 0.25, + primaryRay: 1.0, + secondaryRay: 1.0, + isPT: false, + isMP: false, + interpolationActive: true, + isTurntable: false, + isAxisShowing: false, + isAligned: true, + showScaleBar: true, + showBoundingBox: false, + boundingBoxColor: [255, 255, 0], + backgroundColor: [0, 0, 0], + flipX: 1, + flipY: 1, + flipZ: 1, + channelFolderNames: [], + infoObj: getDefaultImageInfo(), + channelGui: [], + currentImageStore: "", + currentImageName: "", + channelStates: [], +}; + +export const loaderContext = new VolumeLoaderContext( + CACHE_MAX_SIZE, + CONCURRENCY_LIMIT, + PREFETCH_CONCURRENCY_LIMIT +); +// new VolumeLoaderContext( +// CACHE_MAX_SIZE, +// QUEUE_MAX_SIZE, +// QUEUE_MAX_LOW_PRIORITY_SIZE +// ); + + +export const getDefaultChannelState = () => ({ + volumeEnabled: true, + isosurfaceEnabled: false, + colorizeEnabled: false, + colorizeAlpha: 1.0, + isovalue: 128, + opacity: 1.0, + color: [255, 255, 255], + controlPoints: [], +}); + +export const DEFAULT_CHANNEL_COLOR = [128, 128, 128]; // Medium gray diff --git a/src/components/constants.js b/src/components/constants.js new file mode 100644 index 0000000..d125e04 --- /dev/null +++ b/src/components/constants.js @@ -0,0 +1,175 @@ +// constants.js + +// URL search parameter keys +export const CELL_ID_QUERY = "cellId"; +export const FOV_ID_QUERY = "fovId"; +export const CELL_LINE_QUERY = "cellLine"; +export const IMAGE_NAME_QUERY = "name"; + +// View modes +export const YZ_MODE = "YZ"; +export const XZ_MODE = "XZ"; +export const XY_MODE = "XY"; +export const THREE_D_MODE = "3D"; + +// App state values +export const SEGMENTED_CELL = "segmented"; +export const FULL_FIELD_IMAGE = "full field"; + +// Channel setting keys +export const ISO_SURFACE_ENABLED = "isoSurfaceEnabled"; +export const VOLUME_ENABLED = "volumeEnabled"; + +// App State Keys +export const ALPHA_MASK_SLIDER_LEVEL = "alphaMaskSliderLevel"; +export const BRIGHTNESS_SLIDER_LEVEL = "brightnessSliderLevel"; +export const DENSITY_SLIDER_LEVEL = "densitySliderLevel"; +export const LEVELS_SLIDER = "levelsSlider"; +export const MODE = "mode"; +export const AUTO_ROTATE = "autorotate"; +export const MAX_PROJECT = "maxProject"; +export const VOLUMETRIC_RENDER = "volume"; +export const PATH_TRACE = "pathTrace"; +export const LUT_CONTROL_POINTS = "controlPoints"; +export const COLORIZE_ALPHA = "colorizeAlpha"; +export const COLORIZE_ENABLED = "colorizeEnabled"; + +// Volume viewer keys +export const ISO_VALUE = "isovalue"; +export const OPACITY = "opacity"; +export const COLOR = "color"; +export const SAVE_ISO_SURFACE = "saveIsoSurface"; + +// LUT percentiles for remapping intensity values +export const LUT_MIN_PERCENTILE = 0.1; +export const LUT_MAX_PERCENTILE = 0.983; + +// Opacity control for isosurfaces +export const ISOSURFACE_OPACITY_SLIDER_MAX = 255.0; + +// Default values for sliders +export const ALPHA_MASK_SLIDER_3D_DEFAULT = [50]; +export const ALPHA_MASK_SLIDER_2D_DEFAULT = [0]; +export const BRIGHTNESS_SLIDER_LEVEL_DEFAULT = [70]; +export const DENSITY_SLIDER_LEVEL_DEFAULT = [50]; +export const LEVELS_SLIDER_DEFAULT = [35.0, 140.0, 255.0]; + +// Channel group keys +export const OTHER_CHANNEL_KEY = "Other"; +export const SINGLE_GROUP_CHANNEL_KEY = "Channels"; + +// Special channel names +export const CELL_SEGMENTATION_CHANNEL_NAME = "SEG_Memb"; + +export const PRESET_COLORS_0 = [ + [226, 205, 179], // Membrane + [111, 186, 17], // Structure + [141, 163, 192], // DNA + [245, 241, 203], // Brightfield + [224, 227, 209], + [221, 155, 245], + [227, 244, 245], + [255, 98, 0], + [247, 219, 120] + ]; + +// Color presets for channels +export const PRESET_COLORS_1 = [ + [190, 68, 171, 255], + [189, 211, 75, 255], + [61, 155, 169, 255], + [128, 128, 128, 255], + [255, 255, 255, 255], + [239, 27, 45, 255], + [238, 77, 245, 255], + [96, 255, 255, 255] +]; + +export const PRESET_COLORS_2 = [ + [128, 0, 0, 255], + [0, 128, 0, 255], + [0, 0, 128, 255], + [32, 32, 32, 255], + [255, 255, 0, 255], + [255, 0, 255, 255], + [0, 255, 0, 255], + [0, 0, 255, 255] +]; + +export const PRESET_COLORS_3 = [ + [128, 0, 128, 255], + [128, 128, 128, 255], + [0, 128, 128, 255], + [128, 128, 0, 255], + [255, 255, 255, 255], + [255, 0, 0, 255], + [255, 0, 255, 255], + [0, 255, 255, 255] +]; + +// Map of preset color groups +export const PRESET_COLOR_MAP = Object.freeze([ + { colors: PRESET_COLORS_0, name: "Default", key: 0 }, + { colors: PRESET_COLORS_1, name: "Thumbnail colors", key: 1 }, + { colors: PRESET_COLORS_2, name: "RGB colors", key: 2 }, + { colors: PRESET_COLORS_3, name: "White structure", key: 3 } + ]); + +// Application color scheme +export default { + primary1Color: '#0B9AAB', // bright blue + primary2Color: '#827AA3', // aics purple + primary3Color: '#d8e0e2', // light blue gray + accent1Color: '#B8D637', // aics lime green light + accent2Color: '#C1F448', // aics lime green bright + accent3Color: '#d8e0e2', // light blue gray + textColor: '#003057', // dark blue + disabledColor: '#D1D1D1', // dull gray + pickerHeaderColor: '#316773' // cool blue green +}; + +// Default settings and constants +export const DEFAULT_SETTINGS = { + LUT_MIN_PERCENTILE: 0.5, + LUT_MAX_PERCENTILE: 0.983, + ISOSURFACE_OPACITY_SLIDER_MAX: 255.0, + ALPHA_MASK_SLIDER_3D_DEFAULT: [50], + ALPHA_MASK_SLIDER_2D_DEFAULT: [0], + BRIGHTNESS_SLIDER_LEVEL_DEFAULT: [70], + DENSITY_SLIDER_LEVEL_DEFAULT: [50], + LEVELS_SLIDER_DEFAULT: [35.0, 140.0, 255.0], + PLAY_RATE_MS_PER_STEP: 125 + }; + + + // Constants and settings from the reference implementation +export const VIEWER_3D_SETTING = { + groups: [ + { + name: "Observed channels", + channels: [ + { name: "Membrane", match: ["(CMDRP)"], color: "E2CDB3", enabled: true, lut: ["p50", "p98"] }, + { name: "Labeled structure", match: ["(EGFP)|(RFPT)"], color: "6FBA11", enabled: true, lut: ["p50", "p98"] }, + { name: "DNA", match: ["(H3342)"], color: "8DA3C0", enabled: true, lut: ["p50", "p98"] }, + { name: "Bright field", match: ["(100)|(Bright)"], color: "F5F1CB", enabled: false, lut: ["p50", "p98"] }, + ], + }, + { + name: "Segmentation channels", + channels: [ + { name: "Labeled structure", match: ["(SEG_STRUCT)"], color: "E0E3D1", enabled: false, lut: ["p50", "p98"] }, + { name: "Membrane", match: ["(SEG_Memb)"], color: "DD9BF5", enabled: false, lut: ["p50", "p98"] }, + { name: "DNA", match: ["(SEG_DNA)"], color: "E3F4F5", enabled: false, lut: ["p50", "p98"] }, + ], + }, + { + name: "Contour channels", + channels: [ + { name: "Membrane", match: ["(CON_Memb)"], color: "FF6200", enabled: false, lut: ["p50", "p98"] }, + { name: "DNA", match: ["(CON_DNA)"], color: "F7DB78", enabled: false, lut: ["p50", "p98"] }, + ], + }, + ], + maskChannelName: "SEG_Memb", + }; + diff --git a/src/components/useConstructor.js b/src/components/useConstructor.js new file mode 100644 index 0000000..5e06ac3 --- /dev/null +++ b/src/components/useConstructor.js @@ -0,0 +1,13 @@ +import { useRef } from 'react'; + +/** + * For objects which are persistent for the lifetime of the component, not + * a member of state, and require a constructor to create. Wraps `useRef`. + */ +export function useConstructor(constructor) { + const value = useRef(null); + if (value.current === null) { + value.current = constructor(); + } + return value.current; +} diff --git a/src/components/viewSettingsManager.js b/src/components/viewSettingsManager.js new file mode 100644 index 0000000..cf166cf --- /dev/null +++ b/src/components/viewSettingsManager.js @@ -0,0 +1,120 @@ +// viewSettingsManager.js +import { XY_MODE, XZ_MODE, YZ_MODE, THREE_D_MODE } from './constants'; + +class ViewSettingsManager { + constructor() { + this.settings = { + viewMode: THREE_D_MODE, + channelSettings: [], + clipRegion: { + xmin: 0, + xmax: 1, + ymin: 0, + ymax: 1, + zmin: 0, + zmax: 1 + }, + density: 50, + exposure: 70, + maskAlpha: 50, + gamma: [35.0, 140.0, 255.0], + isPT: false, + lastKnown2DMode: XY_MODE, // Track the last used 2D mode + last3DSettings: null // Store 3D settings when switching to 2D + }; + } + + // Save current view settings + saveSettings(settings) { + // If switching modes, store or restore relevant settings + if (settings.viewMode) { + const isCurrently3D = this.settings.viewMode === THREE_D_MODE; + const switchingTo3D = settings.viewMode === THREE_D_MODE; + const switchingFrom3D = isCurrently3D && settings.viewMode !== THREE_D_MODE; + + if (switchingFrom3D) { + // Store 3D settings when switching to 2D + this.settings.last3DSettings = { + clipRegion: { ...this.settings.clipRegion }, + density: this.settings.density, + exposure: this.settings.exposure, + maskAlpha: this.settings.maskAlpha, + // Store any other relevant 3D settings + }; + this.settings.lastKnown2DMode = settings.viewMode; + } else if (switchingTo3D && this.settings.last3DSettings) { + // Restore 3D settings when switching back to 3D + Object.assign(settings, this.settings.last3DSettings); + } + } + + this.settings = { + ...this.settings, + ...settings + }; + } + + // Get current settings + getSettings() { + return { ...this.settings }; + } + + // Update view mode with proper state management + setViewMode(mode) { + if (mode !== THREE_D_MODE) { + this.settings.lastKnown2DMode = mode; + } + this.settings.viewMode = mode; + } + + // Get current view mode + getViewMode() { + return this.settings.viewMode; + } + + // Get last known 2D mode + getLastKnown2DMode() { + return this.settings.lastKnown2DMode; + } + + // Save channel settings + saveChannelSettings(channels) { + this.settings.channelSettings = channels.map(channel => ({ + enabled: channel.enabled, + color: channel.color, + isosurfaceEnabled: channel.isosurfaceEnabled, + isovalue: channel.isovalue, + opacity: channel.opacity + })); + } + + // Get saved channel settings + getChannelSettings() { + return [...this.settings.channelSettings]; + } + + // Convert camera mode to internal view mode + convertCameraModeToViewMode(cameraMode) { + switch (cameraMode) { + case 'X': return YZ_MODE; + case 'Y': return XZ_MODE; + case 'Z': return XY_MODE; + case '3D': return THREE_D_MODE; + default: return THREE_D_MODE; + } + } + + // Convert internal view mode to camera mode + convertViewModeToCameraMode(viewMode) { + switch (viewMode) { + case YZ_MODE: return 'X'; + case XZ_MODE: return 'Y'; + case XY_MODE: return 'Z'; + case THREE_D_MODE: return '3D'; + default: return '3D'; + } + } +} + +const viewSettingsManager = new ViewSettingsManager(); +export default viewSettingsManager; \ No newline at end of file diff --git a/src/components/volumeViewrKeep.js b/src/components/volumeViewrKeep.js new file mode 100644 index 0000000..1816402 --- /dev/null +++ b/src/components/volumeViewrKeep.js @@ -0,0 +1,1070 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { + LoadSpec, + View3d, + VolumeFileFormat, + RENDERMODE_PATHTRACE, + RENDERMODE_RAYMARCH, + VolumeMaker, + Light, + AREA_LIGHT, + SKY_LIGHT, + Lut +} from "@aics/volume-viewer"; +import * as THREE from 'three'; +import { loaderContext, PREFETCH_DISTANCE, MAX_PREFETCH_CHUNKS, myState } from "./appConfig"; +import { useConstructor } from './useConstructor'; +import { Slider, Switch, InputNumber, Row, Col, Collapse, Layout, Button, Select, Input, Tooltip, Spin, Menu, Tabs, Card} from 'antd'; +import axios from 'axios'; +import { API_URL } from '../config'; // Importing API_URL from your config + +// Utility function to concatenate arrays +const concatenateArrays = (arrays) => { + const totalLength = arrays.reduce((acc, arr) => acc + arr.length, 0); + const result = new Uint8Array(totalLength); + let offset = 0; + for (const arr of arrays) { + result.set(arr, offset); + offset += arr.length; + } + return result; +} + +const { Header, Sider, Content, Footer } = Layout; +const { Panel } = Collapse; +const { Option } = Select; +const { Vector3 } = THREE; +const { TabPane } = Tabs; + +const VolumeViewer = () => { + const viewerRef = useRef(null); + const view3D = useConstructor(() => new View3d({ parentElement: viewerRef.current })); + const loadContext = useConstructor(() => loaderContext); + + const [loader, setLoader] = useState(null); + const [fileData, setFileData] = useState({}); + const [selectedBodyPart, setSelectedBodyPart] = useState(null); + const [selectedFile, setSelectedFile] = useState(null); + const [currentVolume, setCurrentVolume] = useState(null); + const [density, setDensity] = useState(myState.density); + const [exposure, setExposure] = useState(myState.exposure); + const [lights, setLights] = useState([ + new Light(SKY_LIGHT), + new Light(AREA_LIGHT) + ]); + const [isPT, setIsPT] = useState(myState.isPT); + const [channels, setChannels] = useState([]); + const [cameraMode, setCameraMode] = useState('3D'); + const [isTurntable, setIsTurntable] = useState(false); + const [showAxis, setShowAxis] = useState(false); + const [showBoundingBox, setShowBoundingBox] = useState(false); + const [showScaleBar, setShowScaleBar] = useState(true); + const [backgroundColor, setBackgroundColor] = useState(myState.backgroundColor); + const [boundingBoxColor, setBoundingBoxColor] = useState(myState.boundingBoxColor); + const [flipX, setFlipX] = useState(1); + const [flipY, setFlipY] = useState(1); + const [flipZ, setFlipZ] = useState(1); + const [gamma, setGamma] = useState([0, 0.5, 1]); + const [clipRegion, setClipRegion] = useState({ + xmin: myState.xmin, + xmax: myState.xmax, + ymin: myState.ymin, + ymax: myState.ymax, + zmin: myState.zmin, + zmax: myState.zmax + }); + const [isPlaying, setIsPlaying] = useState(false); + const [currentFrame, setCurrentFrame] = useState(0); + const [totalFrames, setTotalFrames] = useState(0); + const [timerId, setTimerId] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const [maskAlpha, setMaskAlpha] = useState(myState.maskAlpha); + const [primaryRay, setPrimaryRay] = useState(myState.primaryRay); + const [secondaryRay, setSecondaryRay] = useState(myState.secondaryRay); + const [fov, setFov] = useState(myState.fov); + const [focalDistance, setFocalDistance] = useState(myState.focal_distance); + const [aperture, setAperture] = useState(myState.aperture); + const [samplingRate, setSamplingRate] = useState(myState.samplingRate); + + + + const [skyTopIntensity, setSkyTopIntensity] = useState(myState.skyTopIntensity); + const [skyMidIntensity, setSkyMidIntensity] = useState(myState.skyMidIntensity); + const [skyBotIntensity, setSkyBotIntensity] = useState(myState.skyBotIntensity); + const [skyTopColor, setSkyTopColor] = useState(myState.skyTopColor); + const [skyMidColor, setSkyMidColor] = useState(myState.skyMidColor); + const [skyBotColor, setSkyBotColor] = useState(myState.skyBotColor); + const [lightColor, setLightColor] = useState(myState.lightColor); + const [lightIntensity, setLightIntensity] = useState(myState.lightIntensity); + const [lightTheta, setLightTheta] = useState(myState.lightTheta); + const [lightPhi, setLightPhi] = useState(myState.lightPhi); + + const densitySliderToView3D = (density) => density / 50.0; + + const onChannelDataArrived = (v, channelIndex) => { + view3D.onVolumeData(v, [channelIndex]); + if (channels[channelIndex]) { + view3D.setVolumeChannelEnabled(v, channelIndex, channels[channelIndex].enabled); + } + view3D.updateActiveChannels(v); + view3D.updateLuts(v); + if (v.isLoaded()) { + console.log("Volume " + v.name + " is loaded"); + } + view3D.redraw(); + }; + + const onVolumeCreated = (volume) => { + initializeChannelOptions(volume); + + // volume.channelColorsDefault = volume.imageInfo.channelNames.map(() => DEFAULT_CHANNEL_COLOR); + + setCurrentVolume(volume); + view3D.removeAllVolumes(); + view3D.addVolume(volume); + + + // Log the channel colors to verify the change + console.log("Channel Default Colors:", volume.channelColors); + + + setInitialRenderMode(); + showChannelUI(volume); + view3D.updateActiveChannels(volume); + view3D.updateLuts(volume); + view3D.updateLights(lights); + view3D.updateDensity(volume, densitySliderToView3D(density)); + view3D.updateMaskAlpha(volume, maskAlpha); + view3D.setRayStepSizes(volume, primaryRay, secondaryRay); + view3D.updateExposure(exposure); + view3D.updateCamera(fov, focalDistance, aperture); + // view3D.updatePixelSamplingRate(samplingRate); + view3D.redraw(); + }; + + const loadVolume = async (loadSpec, loader) => { + const volume = await loader.createVolume(loadSpec, onChannelDataArrived); + onVolumeCreated(volume); + + console.log(volume.imageInfo, volume.imageInfo.times) + // Set total frames based on the volume's metadata (assuming 'times' represents the number of frames) + setTotalFrames(volume.imageInfo.times || 1); + await loader.loadVolumeData(volume); + }; + + const loadVolumeFromServer = async (url) => { + setIsLoading(true); + try { + const loadSpec = new LoadSpec(); + const fileExtension = url.split('.').pop(); + const volumeFileType = (fileExtension === 'tiff' || fileExtension === 'tif' || fileExtension === 'ome.tiff' || fileExtension === 'ome.tif') ? VolumeFileFormat.TIFF : VolumeFileFormat.ZARR; + const loader = await loadContext.createLoader(url, { + fileType: volumeFileType, + fetchOptions: { maxPrefetchDistance: PREFETCH_DISTANCE, maxPrefetchChunks: MAX_PREFETCH_CHUNKS }, + }); + + setLoader(loader); + await loadVolume(loadSpec, loader); + } catch (error) { + console.error('Error loading volume:', error); + } finally { + setIsLoading(false); + } + }; + + const createTestVolume = () => { + const sizeX = 64; + const sizeY = 64; + const sizeZ = 64; + const imgData = { + name: "AICS-10_5_5", + sizeX, + sizeY, + sizeZ, + sizeC: 3, + physicalPixelSize: [1, 1, 1], + spatialUnit: "", + channelNames: ["DRAQ5", "EGFP", "SEG_Memb"], + }; + + const channelVolumes = [ + VolumeMaker.createSphere(sizeX, sizeY, sizeZ, 24), + VolumeMaker.createTorus(sizeX, sizeY, sizeZ, 24, 8), + VolumeMaker.createCone(sizeX, sizeY, sizeZ, 24, 24), + ]; + + const alldata = concatenateArrays(channelVolumes); + return { + metadata: imgData, + data: { + dtype: "uint8", + shape: [channelVolumes.length, sizeZ, sizeY, sizeX], + buffer: new DataView(alldata.buffer), + }, + }; + }; + + const fetchFiles = async () => { + try { + const response = await axios.get(`${API_URL}/files`); + setFileData(response.data); + } catch (error) { + console.error('Error fetching files:', error); + } + }; + + const handleFileSelect = async (bodyPart, file) => { + setSelectedBodyPart(bodyPart); + setSelectedFile(file); + await loadVolumeFromServer(`${API_URL}/${bodyPart}/${file}`); + }; + + useEffect(() => { + fetchFiles(); + }, []); + + useEffect(() => { + if (viewerRef.current) { + const container = viewerRef.current; + container.appendChild(view3D.getDOMElement()); + + const handleResize = () => view3D.resize(); + window.addEventListener("resize", handleResize); + + view3D.resize(); + + return () => { + window.removeEventListener("resize", handleResize); + if (view3D.getDOMElement().parentNode) { + view3D.getDOMElement().parentNode.removeChild(view3D.getDOMElement()); + } + view3D.removeAllVolumes(); + }; + } + }, [viewerRef, view3D]); + + useEffect(() => { + if (currentVolume) { + view3D.updateDensity(currentVolume, densitySliderToView3D(density)); + view3D.redraw(); + } + }, [density]); + + useEffect(() => { + if (currentVolume) { + view3D.updateExposure(exposure); + view3D.redraw(); + } + }, [currentVolume, exposure, view3D]); + + useEffect(() => { + view3D.setVolumeRenderMode(isPT ? RENDERMODE_PATHTRACE : RENDERMODE_RAYMARCH); + view3D.redraw(); + }, [isPT, view3D]); + + useEffect(() => { + view3D.updateLights(lights); + view3D.redraw(); + }, [lights, view3D]); + + useEffect(() => { + if (currentVolume) { + view3D.updateActiveChannels(currentVolume); + view3D.updateLuts(currentVolume); + view3D.redraw(); + } + }, [channels]); + + useEffect(() => { + view3D.setCameraMode(cameraMode); + }, [cameraMode]); + + useEffect(() => { + view3D.setAutoRotate(isTurntable); + }, [isTurntable, view3D]); + + useEffect(() => { + view3D.setShowAxis(showAxis); + }, [showAxis, view3D]); + + useEffect(() => { + if (currentVolume) { + view3D.setShowBoundingBox(currentVolume, showBoundingBox); + } + }, [currentVolume, showBoundingBox, view3D]); + + useEffect(() => { + view3D.setShowScaleBar(showScaleBar); + }, [showScaleBar, view3D]); + + useEffect(() => { + view3D.setBackgroundColor(backgroundColor); + }, [backgroundColor, view3D]); + + useEffect(() => { + if (currentVolume) { + view3D.setBoundingBoxColor(currentVolume, boundingBoxColor); + } + }, [boundingBoxColor]); + + useEffect(() => { + if (currentVolume) { + view3D.setFlipVolume(currentVolume, flipX, flipY, flipZ); + } + }, [flipX, flipY, flipZ]); + + useEffect(() => { + if (currentVolume) { + const gammaValues = gammaSliderToImageValues(gamma); + view3D.setGamma(currentVolume, gammaValues[0], gammaValues[1], gammaValues[2]); + } + }, [gamma]); + + useEffect(() => { + if (currentVolume) { + view3D.updateClipRegion( + currentVolume, + clipRegion.xmin, + clipRegion.xmax, + clipRegion.ymin, + clipRegion.ymax, + clipRegion.zmin, + clipRegion.zmax + ); + } + }, [clipRegion]); + + useEffect(() => { + if (currentVolume) { + view3D.updateCamera(fov, focalDistance, aperture); + view3D.redraw(); + } + }, [fov, focalDistance, aperture]); + + + useEffect(() => { + if (currentVolume) { + view3D.setRayStepSizes(currentVolume, primaryRay, secondaryRay); + view3D.redraw(); + } + }, [primaryRay, secondaryRay]); + + useEffect(() => { + if (currentVolume) { + view3D.updateMaskAlpha(currentVolume, maskAlpha); + view3D.redraw(); + } + }, [maskAlpha]) + + useEffect(() => { + if (view3D && lights[0]) { + const skyLight = lights[0]; + skyLight.mColorTop = new Vector3( + (skyTopColor[0] / 255.0) * skyTopIntensity, + (skyTopColor[1] / 255.0) * skyTopIntensity, + (skyTopColor[2] / 255.0) * skyTopIntensity + ); + skyLight.mColorMiddle = new Vector3( + (skyMidColor[0] / 255.0) * skyMidIntensity, + (skyMidColor[1] / 255.0) * skyMidIntensity, + (skyMidColor[2] / 255.0) * skyMidIntensity + ); + skyLight.mColorBottom = new Vector3( + (skyBotColor[0] / 255.0) * skyBotIntensity, + (skyBotColor[1] / 255.0) * skyBotIntensity, + (skyBotColor[2] / 255.0) * skyBotIntensity + ); + view3D.updateLights(lights); + // view3D.redraw(); + console.log([skyTopColor, skyTopIntensity, skyMidColor, skyMidIntensity, skyBotColor, skyBotIntensity]); + } + + }, [skyTopColor, skyTopIntensity, skyMidColor, skyMidIntensity, skyBotColor, skyBotIntensity]); + + // useEffect for area light + useEffect(() => { + if (view3D && lights[1]) { + const areaLight = lights[1]; + areaLight.mColor = new Vector3( + (lightColor[0] / 255.0) * lightIntensity, + (lightColor[1] / 255.0) * lightIntensity, + (lightColor[2] / 255.0) * lightIntensity + ); + areaLight.mTheta = (lightTheta * Math.PI) / 180.0; + areaLight.mPhi = (lightPhi * Math.PI) / 180.0; + view3D.updateLights(lights); + // view3D.redraw(); + } + console.log([lightColor, lightIntensity, lightTheta, lightPhi]); + }, [lightColor, lightIntensity, lightTheta, lightPhi]); + + const setInitialRenderMode = () => { + view3D.setVolumeRenderMode(isPT ? RENDERMODE_PATHTRACE : RENDERMODE_RAYMARCH); + view3D.setMaxProjectMode(currentVolume, false); + }; + + const DEFAULT_CHANNEL_COLOR = [128, 128, 128]; // Medium gray + + const showChannelUI = (volume) => { + const channelGui = volume.imageInfo.channelNames.map((name, index) => ({ + name, + enabled: index < 3, + colorD: volume.channelColorsDefault[index] || DEFAULT_CHANNEL_COLOR, + colorS: [0, 0, 0], + colorE: [0, 0, 0], + glossiness: 0, + window: 1, + level: 0.5, + isovalue: 128, + isosurface: false + })); + setChannels(channelGui); + + // Log channel colors for verification + channelGui.forEach((channel, index) => { + console.log(`Channel ${index} (${channel.name}) color:`, channel.colorD); + }); + }; + + const updateChannel = (index, key, value) => { + const updatedChannels = [...channels]; + updatedChannels[index][key] = value; + setChannels(updatedChannels); + + if (currentVolume) { + if (key === 'enabled') { + view3D.setVolumeChannelEnabled(currentVolume, index, value); + } else if (key === 'isosurface') { + view3D.setVolumeChannelOptions(currentVolume, index, { isosurfaceEnabled: value }); + if (value) { + view3D.createIsosurface(currentVolume, index, updatedChannels[index].isovalue, 1.0); + } else { + view3D.clearIsosurface(currentVolume, index); + } + } else if (['colorD', 'colorS', 'colorE', 'glossiness'].includes(key)) { + view3D.updateChannelMaterial( + currentVolume, + index, + updatedChannels[index].colorD, + updatedChannels[index].colorS, + updatedChannels[index].colorE, + updatedChannels[index].glossiness + ); + view3D.updateMaterial(currentVolume); + } else if (key === 'window' || key === 'level') { + const lut = new Lut().createFromWindowLevel( + updatedChannels[index].window, + updatedChannels[index].level + ); + currentVolume.setLut(index, lut); + view3D.updateLuts(currentVolume); + } + view3D.redraw(); + } + } + + + const updateChannelOptions = (index, options) => { + const updatedChannels = [...channels]; + updatedChannels[index] = { ...updatedChannels[index], ...options }; + setChannels(updatedChannels); + + if (view3D) { + view3D.setVolumeChannelOptions(index, options); + if (options.isosurfaceEnabled !== undefined) { + if (options.isosurfaceEnabled) { + const channel = updatedChannels[index]; + view3D.createIsosurface( + index, + channel.color, + channel.isovalue, + channel.isosurfaceOpacity, + channel.isosurfaceOpacity < 0.95 + ); + } else { + view3D.clearIsosurface(index); + } + } + if (options.isovalue !== undefined || options.isosurfaceOpacity !== undefined) { + const channel = updatedChannels[index]; + view3D.updateIsosurface(index, channel.isovalue); + view3D.updateChannelMaterial( + index, + channel.color, + channel.specularColor, + channel.emissiveColor, + channel.glossiness + ); + view3D.updateOpacity(index, channel.isosurfaceOpacity); + } + view3D.redraw(); + } + }; + + const initializeChannelOptions = (volume) => { + const channelOptions = volume.imageInfo.channelNames.map((name, index) => ({ + name, + enabled: index < 3, + color: volume.channelColorsDefault[index] || [128, 128, 128], + specularColor: [0, 0, 0], + emissiveColor: [0, 0, 0], + glossiness: 0, + isosurfaceEnabled: false, + isovalue: 127, + isosurfaceOpacity: 1.0 + })); + setChannels(channelOptions); + }; + + const updateIsovalue = (index, isovalue) => { + if (currentVolume) { + view3D.updateIsosurface(currentVolume, index, isovalue); + view3D.redraw(); + } + }; + + // Histogram-based LUT adjustments + const updateChannelLut = (index, type) => { + if (currentVolume) { + let lut; + if (type === 'autoIJ') { + const [hmin, hmax] = currentVolume.getHistogram(index).findAutoIJBins(); + lut = new Lut().createFromMinMax(hmin, hmax); + } else if (type === 'auto0') { + const [b, e] = currentVolume.getHistogram(index).findAutoMinMax(); + lut = new Lut().createFromMinMax(b, e); + } else if (type === 'bestFit') { + const [hmin, hmax] = currentVolume.getHistogram(index).findBestFitBins(); + lut = new Lut().createFromMinMax(hmin, hmax); + } else if (type === 'pct50_98') { + const hmin = currentVolume.getHistogram(index).findBinOfPercentile(0.5); + const hmax = currentVolume.getHistogram(index).findBinOfPercentile(0.983); + lut = new Lut().createFromMinMax(hmin, hmax); + } + + currentVolume.setLut(index, lut); + view3D.updateLuts(currentVolume); + view3D.redraw(); + } + }; + + const setCameraModeHandler = (mode) => { + setCameraMode(mode); + }; + + const toggleTurntable = () => { + setIsTurntable(!isTurntable); + }; + + const toggleAxis = () => { + setShowAxis(!showAxis); + }; + + const toggleBoundingBox = () => { + setShowBoundingBox(!showBoundingBox); + }; + + const toggleScaleBar = () => { + setShowScaleBar(!showScaleBar); + }; + + const updateBackgroundColor = (color) => { + setBackgroundColor(color); + }; + + const updateBoundingBoxColor = (color) => { + setBoundingBoxColor(color); + }; + + const flipVolume = (axis) => { + if (axis === 'X') { + setFlipX(flipX * -1); + } else if (axis === 'Y') { + setFlipY(flipY * -1); + } else if (axis === 'Z') { + setFlipZ(flipZ * -1); + } + }; + + const gammaSliderToImageValues = (sliderValues) => { + let min = Number(sliderValues[0]); + let mid = Number(sliderValues[1]); + let max = Number(sliderValues[2]); + if (mid > max || mid < min) { + mid = 0.5 * (min + max); + } + const div = 255; + min /= div; + max /= div; + mid /= div; + const diff = max - min; + const x = (mid - min) / diff; + let scale = 4 * x * x; + if ((mid - 0.5) * (mid - 0.5) < 0.0005) { + scale = 1.0; + } + return [min, max, scale]; + }; + + const updateGamma = (newGamma) => { + setGamma(newGamma); + }; + + const captureScreenshot = () => { + view3D.capture((dataUrl) => { + const anchor = document.createElement("a"); + anchor.href = dataUrl; + anchor.download = "screenshot.png"; + anchor.click(); + }); + }; + + const updateClipRegion = (key, value) => { + const updatedClipRegion = { ...clipRegion, [key]: value }; + setClipRegion(updatedClipRegion); + }; + + const goToFrame = (frame) => { + if (frame >= 0 && frame < totalFrames) { + view3D.setTime(currentVolume, frame); + setCurrentFrame(frame); + } + }; + + const goToZSlice = (slice) => { + if (currentVolume && view3D.setZSlice(currentVolume, slice)) { + // Z slice updated successfully + const zSlider = document.getElementById("zSlider"); + const zInput = document.getElementById("zValue"); + + if (zInput) { + zInput.value = slice; + } + if (zSlider) { + zSlider.value = slice; + } + } else { + console.log('Failed to update Z slice'); + } + }; + + const playTimeSeries = () => { + if (timerId) { + clearInterval(timerId); + } + setIsPlaying(true); + const newTimerId = setInterval(() => { + setCurrentFrame((prevFrame) => { + const nextFrame = (prevFrame + 1) % totalFrames; + view3D.setTime(currentVolume, nextFrame); + return nextFrame; + }); + }, 80); + setTimerId(newTimerId); + }; + + const pauseTimeSeries = () => { + if (timerId) { + clearInterval(timerId); + setTimerId(null); + } + setIsPlaying(false); + }; + + const rgbToHex = (r, g, b) => { + const toHex = (component) => { + const hex = Math.round(component).toString(16); + return hex.length === 1 ? '0' + hex : hex; // Ensures two digits + }; + + // Ensure r, g, b are valid numbers and fall back to 0 if undefined or invalid + r = isNaN(r) ? 0 : r; + g = isNaN(g) ? 0 : g; + b = isNaN(b) ? 0 : b; + + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; + }; + + const updatePixelSamplingRate = (rate) => { + setSamplingRate(rate); + view3D.updatePixelSamplingRate(rate); + view3D.redraw(); + }; + + + const updateSkyLight = (position, intensity, color) => { + if (position === 'top') { + setSkyTopIntensity(intensity); + setSkyTopColor(color); + } else if (position === 'mid') { + setSkyMidIntensity(intensity); + setSkyMidColor(color); + } else if (position === 'bot') { + setSkyBotIntensity(intensity); + setSkyBotColor(color); + } + updateLights(); + }; + + const updateAreaLight = (intensity, color, theta, phi) => { + setLightIntensity(intensity); + setLightColor(color); + setLightTheta(theta); + setLightPhi(phi); + updateLights(); + }; + + const updateLights = () => { + const updatedLights = [...lights]; + // Update sky light + updatedLights[0].mColorTop = new Vector3( + (skyTopColor[0] / 255.0) * skyTopIntensity, + (skyTopColor[1] / 255.0) * skyTopIntensity, + (skyTopColor[2] / 255.0) * skyTopIntensity + ); + updatedLights[0].mColorMiddle = new Vector3( + (skyMidColor[0] / 255.0) * skyMidIntensity, + (skyMidColor[1] / 255.0) * skyMidIntensity, + (skyMidColor[2] / 255.0) * skyMidIntensity + ); + updatedLights[0].mColorBottom = new Vector3( + (skyBotColor[0] / 255.0) * skyBotIntensity, + (skyBotColor[1] / 255.0) * skyBotIntensity, + (skyBotColor[2] / 255.0) * skyBotIntensity + ); + + // Update area light + updatedLights[1].mColor = new Vector3( + (lightColor[0] / 255.0) * lightIntensity, + (lightColor[1] / 255.0) * lightIntensity, + (lightColor[2] / 255.0) * lightIntensity + ); + updatedLights[1].mTheta = (lightTheta * Math.PI) / 180.0; + updatedLights[1].mPhi = (lightPhi * Math.PI) / 180.0; + + setLights(updatedLights); + view3D.updateLights(updatedLights); + view3D.redraw(); + }; + + return ( + +
+ + Home + About + Help + +
+ + + + {Object.keys(fileData).map((bodyPart) => ( + + {fileData[bodyPart].map((file) => ( +
handleFileSelect(bodyPart, file)} + > + {file} +
+ ))} +
+ ))} +
+
+ + +
+
+ +
+
+
+ + + +
+
+ setIsPT(checked)} /> Path Trace +
+
+ Density: +
+
+ Mask Alpha: +
+
+ Primary Ray: +
+
+ Secondary Ray: +
+
+ Exposure: +
+
+
+ +
+
+ FOV: +
+
+ Focal Distance: +
+
+ Aperture: +
+
+ Pixel Sampling Rate: +
+
+ Camera Mode: + +
+
+
+ + + +
+
+ Top Intensity: + updateSkyLight('top', value, skyTopColor)} + /> +
+
+ Top Color: + updateSkyLight('top', skyTopIntensity, e.target.value.match(/[A-Za-z0-9]{2}/g).map(v => parseInt(v, 16)))} + /> +
+
+ Mid Intensity: + updateSkyLight('mid', value, skyMidColor)} + /> +
+
+ Mid Color: + updateSkyLight('mid', skyMidIntensity, e.target.value.match(/[A-Za-z0-9]{2}/g).map(v => parseInt(v, 16)))} + /> +
+
+
+ +
+
+ Intensity: + updateAreaLight(value, lightColor, lightTheta, lightPhi)} + /> +
+
+ Color: + updateAreaLight(lightIntensity, e.target.value.match(/[A-Za-z0-9]{2}/g).map(v => parseInt(v, 16)), lightTheta, lightPhi)} + /> +
+
+ Theta (deg): + updateAreaLight(lightIntensity, lightColor, value, lightPhi)} + /> +
+
+ Phi (deg): + updateAreaLight(lightIntensity, lightColor, lightTheta, value)} + /> +
+
+
+
+
+ +
+ + + + + + + +
+ Background Color: + c.toString(16).padStart(2, '0')).join('')}`} + onChange={(e) => updateBackgroundColor(e.target.value.match(/.{1,2}/g).map(c => parseInt(c, 16)))} /> +
+
+ Bounding Box Color: + c.toString(16).padStart(2, '0')).join('')}`} + onChange={(e) => updateBoundingBoxColor(e.target.value.match(/.{1,2}/g).map(c => parseInt(c, 16)))} /> +
+
+
+ +
+ {channels.map((channel, index) => ( +
+
+ updateChannel(index, 'enabled', checked)} /> Enable +
+ {channel.isosurfaceEnabled && ( + <> +
+ Isovalue: + updateChannelOptions(index, { isovalue: value })} + /> +
+
+ Isosurface Opacity: + updateChannelOptions(index, { isosurfaceOpacity: value })} + /> +
+ + )} +
+ Diffuse Color: + updateChannel(index, 'colorD', e.target.value.match(/.{1,2}/g).map(c => parseInt(c, 16)))} /> +
+
+ + + + +
+
+ ))} +
+
+ +
+
+ X Min: + updateClipRegion('xmin', value)} /> +
+
+ X Max: + updateClipRegion('xmax', value)} /> +
+
+ Y Min: + updateClipRegion('ymin', value)} /> +
+
+ Y Max: + updateClipRegion('ymax', value)} /> +
+
+ Z Min: + updateClipRegion('zmin', value)} /> +
+
+ Z Max: + updateClipRegion('zmax', value)} /> +
+
+
+ +
+
+ + + + +
+
+ Frame: +
+
+ Z Slice: +
+
+ +
+
+
+ +
+
+ Min: updateGamma([value, gamma[1], gamma[2]])} /> +
+
+ Mid: updateGamma([gamma[0], value, gamma[2]])} /> +
+
+ Max: updateGamma([gamma[0], gamma[1], value])} /> +
+
+
+
+
+
+
+
+
+
+ ); +} + +export default VolumeViewer; \ No newline at end of file diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..5dacc8e --- /dev/null +++ b/src/config.js @@ -0,0 +1,2 @@ +// API URL +export const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:5000'; \ No newline at end of file diff --git a/src/utils/colorUtils.js b/src/utils/colorUtils.js new file mode 100644 index 0000000..faf14a8 --- /dev/null +++ b/src/utils/colorUtils.js @@ -0,0 +1,57 @@ +// src/utils/colorUtils.js + +import { Lut } from "@aics/volume-viewer"; + +export const controlPointsToLut = (controlPoints) => { + if (!controlPoints || controlPoints.length < 2) { + return new Lut().createFromMinMax(0, 255); + } + + const lut = new Lut(); + const sortedPoints = [...controlPoints].sort((a, b) => a.x - b.x); + + // Ensure the LUT spans the full range + if (sortedPoints[0].x > 0) { + sortedPoints.unshift({ ...sortedPoints[0], x: 0 }); + } + if (sortedPoints[sortedPoints.length - 1].x < 1) { + sortedPoints.push({ ...sortedPoints[sortedPoints.length - 1], x: 1 }); + } + + // Create LUT from control points + for (let i = 0; i < sortedPoints.length - 1; i++) { + const p1 = sortedPoints[i]; + const p2 = sortedPoints[i + 1]; + + const steps = Math.ceil((p2.x - p1.x) * 255); + for (let j = 0; j < steps; j++) { + const t = j / steps; + const x = p1.x + t * (p2.x - p1.x); + const y = p1.y + t * (p2.y - p1.y); + + // Interpolate colors + const r = p1.color[0] + t * (p2.color[0] - p1.color[0]); + const g = p1.color[1] + t * (p2.color[1] - p1.color[1]); + const b = p1.color[2] + t * (p2.color[2] - p1.color[2]); + + const index = Math.floor(x * 255); + lut.lut[index] = [r, g, b, 255]; + } + } + + return lut; +}; + +export const rgbaFromArray = (arr) => { + return { + r: arr[0] || 0, + g: arr[1] || 0, + b: arr[2] || 0, + a: arr[3] ? arr[3] / 255 : 1 + }; +}; + +export const rgbaToString = (rgba) => { + const { r, g, b, a } = rgba; + return `rgba(${r},${g},${b},${a})`; +}; \ No newline at end of file