diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 00000000..c051f7df --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,24 @@ +name: Python Tests + +on: [push, pull_request] + +jobs: + pytest: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./applications/visualizer/backend + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install dependencies and dev dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -r requirements-dev.txt + - name: Test with pytest + run: | + python -m pytest \ No newline at end of file diff --git a/applications/visualizer/api/openapi.json b/applications/visualizer/api/openapi.json index 90d68f86..2e6b80c7 100644 --- a/applications/visualizer/api/openapi.json +++ b/applications/visualizer/api/openapi.json @@ -530,9 +530,12 @@ "title": "Datasetids", "type": "array" }, - "model3DUrl": { - "title": "Model3Durl", - "type": "string" + "model3DUrls": { + "items": { + "type": "string" + }, + "title": "Model3Durls", + "type": "array" }, "nclass": { "maxLength": 30, @@ -568,7 +571,7 @@ "required": [ "name", "datasetIds", - "model3DUrl", + "model3DUrls", "nclass", "neurotransmitter", "type" diff --git a/applications/visualizer/api/openapi.yaml b/applications/visualizer/api/openapi.yaml index 9171cbf8..617f653f 100644 --- a/applications/visualizer/api/openapi.yaml +++ b/applications/visualizer/api/openapi.yaml @@ -142,9 +142,11 @@ components: default: false title: Intail type: boolean - model3DUrl: - title: Model3Durl - type: string + model3DUrls: + items: + type: string + title: Model3Durls + type: array name: title: Name type: string @@ -163,7 +165,7 @@ components: required: - name - datasetIds - - model3DUrl + - model3DUrls - nclass - neurotransmitter - type diff --git a/applications/visualizer/backend/api/api.py b/applications/visualizer/backend/api/api.py index bcb601d9..c738a6d1 100644 --- a/applications/visualizer/backend/api/api.py +++ b/applications/visualizer/backend/api/api.py @@ -14,7 +14,7 @@ Neuron as NeuronModel, Connection as ConnectionModel, ) -from .services.connectivity import query_connections +from .services.connectivity import query_nematode_connections class ErrorMessage(Schema): @@ -132,7 +132,7 @@ def annotate_neurons(neurons: BaseManager[NeuronModel]) -> None: for neuron in neurons: name = neuron.name neuron.dataset_ids = neurons_dataset_ids[name] # type: ignore - neuron.model3D_url = settings.NEURON_REPRESENTATION_3D_URL_FORMAT.format(name=name) # type: ignore + neuron.model3D_urls = [settings.NEURON_REPRESENTATION_3D_URL_FORMAT.format(name=name)] # type: ignore def neurons_from_datasets( @@ -218,7 +218,7 @@ def get_connections( include_annotations: bool = False, ): """Gets the connections of a dedicated Dataset""" - return query_connections( + return query_nematode_connections( [c.strip() for c in cells.split(",")], [d.strip() for d in dataset_ids.split(",")], [d.strip() for d in dataset_type.split(",")], diff --git a/applications/visualizer/backend/api/schemas.py b/applications/visualizer/backend/api/schemas.py index 65e77b39..36705c32 100644 --- a/applications/visualizer/backend/api/schemas.py +++ b/applications/visualizer/backend/api/schemas.py @@ -56,7 +56,7 @@ class Meta: class Neuron(ModelSchema, BilingualSchema): name: str dataset_ids: list[str] - model3D_url: str + model3D_urls: list[str] class Meta: model = NeuronModel diff --git a/applications/visualizer/backend/api/services/connectivity.py b/applications/visualizer/backend/api/services/connectivity.py index b45b3f8f..1f710a12 100644 --- a/applications/visualizer/backend/api/services/connectivity.py +++ b/applications/visualizer/backend/api/services/connectivity.py @@ -1,11 +1,12 @@ from collections import defaultdict +from api.models import Annotation, Connection -from django.db.models import Q -from api.models import Annotation, Connection +## This module was converted from the original nemanode code found here: +## https://github.com/zhenlab-ltri/NemaNode/blob/master/src/server/db/nematode-connections.js -def query_connections( +def query_nematode_connections( cells: list[str], dataset_ids: list[str], dataset_type: list[str], @@ -14,7 +15,21 @@ def query_connections( include_neighboring_cells: bool, include_annotations: bool, ): - connections = _query_raw_connections( + if not cells: + return [] + + annotations_map = defaultdict(list) + if include_annotations: + annotations = query_annotations(cells, include_neighboring_cells, dataset_type) + for annotation in annotations: + pre = annotation["pre"] + post = annotation["post"] + connection_type = annotation["type"] + annotation_type = annotation["annotation"] + key = get_connection_primary_key(pre, post, connection_type) + annotations_map[key].append(annotation_type) + + raw_connections = query_connections( cells, dataset_ids, include_neighboring_cells, @@ -22,100 +37,158 @@ def query_connections( threshold_electrical, ) - annotations_map = _query_annotations( - cells, dataset_type, include_annotations, include_neighboring_cells - ) - + connections = [] + # Group raw_connections by the connection key grouped_connections = defaultdict(list) - for connection in connections: - key = _get_connection_key(connection.pre, connection.post, connection.type) - grouped_connections[key].append(connection) + for raw_conn in raw_connections: + key = get_connection_primary_key( + raw_conn["pre"], raw_conn["post"], raw_conn["type"] + ) + grouped_connections[key].append(raw_conn) - response_data = [] - for key, group in grouped_connections.items(): - synapses = {conn.dataset_id: conn.synapses for conn in group} + for key, grouped in grouped_connections.items(): + pre = grouped[0]["pre"] + post = grouped[0]["post"] + type_ = grouped[0]["type"] + annotations = annotations_map[key] if include_annotations else [] - # It's safe to access the first item directly as group won't be empty by design of the grouping process - connection = group[0] - response_data.append( + # Accumulate all synapses for the grouped connections + synapses = {g["dataset_id"]: g["synapses"] for g in grouped} + + connections.append( { - "pre": connection.pre, - "post": connection.post, - "type": connection.type, - "annotations": ( - annotations_map.get(key, []) if include_annotations else [] - ), + "pre": pre, + "post": post, + "type": type_, + "annotations": annotations, "synapses": synapses, } ) + gap_junctions = [c for c in connections if c["type"] == "electrical"] + chemical_synapses = [c for c in connections if c["type"] == "chemical"] - return response_data + merged_gap_junctions = merge_gap_junctions(gap_junctions) + return merged_gap_junctions + chemical_synapses -# SELECT pre, post, type, annotation -# FROM annotations -# WHERE (pre in (${cells}) -# ${includeNeighboringCells ? 'OR' : 'AND'} post in (${cells})) -# AND collection in (${datasetType}) +def get_connection_primary_key(pre, post, type_): + return hash(f"{pre}-{post}-{type_}") -def _query_annotations( - cells, dataset_type, include_annotations, include_neighboring_cells -): - if not include_annotations: - return {} - annotations_map = defaultdict(list) - annotations = Annotation.objects.filter( - ( - Q(pre__in=cells) | Q(post__in=cells) - if include_neighboring_cells - else Q(pre__in=cells, post__in=cells) - ), - collection__in=dataset_type, - ) - for annotation in annotations: - key = _get_connection_key(annotation.pre, annotation.post, annotation.type) - annotations_map[key].append(annotation.annotation) - return annotations_map - - -# SELECT c.pre, c.post, c.type, c.dataset_id, c.synapses from ( -# SELECT pre, post, type -# FROM connections -# WHERE (pre IN (${cells}) -# ${includeNeighboringCells ? 'OR' : 'AND'} post IN (${cells})) -# AND dataset_id IN (${datasetIds}) -# AND ( -# (type = 'chemical' && synapses >= ${thresholdChemical}) -# OR (type = 'electrical' && synapses >= ${thresholdElectrical}) -# ) -# GROUP BY pre, post, type -# ) f -# LEFT JOIN connections c ON f.pre = c.pre AND f.post = c.post AND f.type = c.type -# WHERE c.dataset_id IN (${datasetIds}) - - -def _query_raw_connections( +def merge_gap_junctions(gap_junctions): + gap_junctions_key_map = {} + + for gj in gap_junctions: + pre, post = gj["pre"], gj["post"] + synapses = gj["synapses"] + key = "$".join(sorted([pre, post])) + + if key not in gap_junctions_key_map: + gap_junctions_key_map[key] = gj + else: + for dataset, synapse_count in synapses.items(): + if dataset in gap_junctions_key_map[key]["synapses"]: + gap_junctions_key_map[key]["synapses"][dataset] += synapse_count + else: + gap_junctions_key_map[key]["synapses"][dataset] = synapse_count + + merged = [] + for gj_key, gj in gap_junctions_key_map.items(): + pre, post = gj_key.split("$") + type_, synapses, annotations = gj["type"], gj["synapses"], gj["annotations"] + merged.append( + { + "pre": pre, + "post": post, + "type": type_, + "synapses": synapses, + "annotations": annotations, + } + ) + + return merged + + +def query_annotations(cells, include_neighboring_cells, dataset_type): + # Prepare the SQL query + sql_query = f""" + SELECT id, pre, post, type, annotation + FROM api_annotation + WHERE (pre IN ({', '.join(['%s'] * len(cells))}) + {'OR' if include_neighboring_cells else 'AND'} post IN ({', '.join(['%s'] * len(cells))})) + AND collection IN ({', '.join(['%s'] * len(dataset_type))}) + """ + + # Combine all parameters into a single list for passing to the query + params = cells + cells + dataset_type + + # Execute the raw query using Django's raw() method + annotations = Annotation.objects.raw(sql_query, params) + + # Convert the result to a list of dictionaries + results = [ + { + "pre": annotation.pre, + "post": annotation.post, + "type": annotation.type, + "annotation": annotation.annotation, + } + for annotation in annotations + ] + + return results + + +def query_connections( cells, dataset_ids, include_neighboring_cells, threshold_chemical, threshold_electrical, ): - connection_query = ( - Q(pre__in=cells) | Q(post__in=cells) - if include_neighboring_cells - else Q(pre__in=cells, post__in=cells) - ) - connection_query &= Q(dataset_id__in=dataset_ids) - connection_query &= Q(type="chemical", synapses__gte=threshold_chemical) | Q( - type="electrical", synapses__gte=threshold_electrical + # Prepare the SQL query + sql_query = f""" + SELECT c.id, c.pre, c.post, c.type, c.dataset_id, c.synapses + FROM ( + SELECT pre, post, type + FROM api_connection + WHERE (pre IN ({', '.join(['%s'] * len(cells))}) + {'OR' if include_neighboring_cells else 'AND'} post IN ({', '.join(['%s'] * len(cells))})) + AND dataset_id IN ({', '.join(['%s'] * len(dataset_ids))}) + AND ( + (type = 'chemical' AND synapses >= %s) + OR (type = 'electrical' AND synapses >= %s) + ) + GROUP BY pre, post, type + ) f + LEFT JOIN api_connection c ON f.pre = c.pre AND f.post = c.post AND f.type = c.type + WHERE c.dataset_id IN ({', '.join(['%s'] * len(dataset_ids))}) + """ + + # Combine all parameters into a single list for passing to the query + params = ( + cells + + cells + + dataset_ids + + [threshold_chemical, threshold_electrical] + + dataset_ids ) - return ( - Connection.objects.filter(connection_query).select_related("dataset").distinct() - ) - -def _get_connection_key(pre, post, connection_type): - return pre, post, connection_type + # Execute the raw query using Django's raw() method + connections = Connection.objects.raw(sql_query, params) + + # Convert the result to a list of dictionaries (similar to what Django ORM's .values() would return) + results = [ + { + "id": connection.id, + "pre": connection.pre, + "post": connection.post, + "type": connection.type, + "dataset_id": connection.dataset_id, + "synapses": connection.synapses, + } + for connection in connections + ] + + return results diff --git a/applications/visualizer/backend/openapi/openapi.json b/applications/visualizer/backend/openapi/openapi.json index 90d68f86..2e6b80c7 100644 --- a/applications/visualizer/backend/openapi/openapi.json +++ b/applications/visualizer/backend/openapi/openapi.json @@ -530,9 +530,12 @@ "title": "Datasetids", "type": "array" }, - "model3DUrl": { - "title": "Model3Durl", - "type": "string" + "model3DUrls": { + "items": { + "type": "string" + }, + "title": "Model3Durls", + "type": "array" }, "nclass": { "maxLength": 30, @@ -568,7 +571,7 @@ "required": [ "name", "datasetIds", - "model3DUrl", + "model3DUrls", "nclass", "neurotransmitter", "type" diff --git a/applications/visualizer/backend/tests/test_neurons.py b/applications/visualizer/backend/tests/test_neurons.py index 860ff934..2b70b4a6 100644 --- a/applications/visualizer/backend/tests/test_neurons.py +++ b/applications/visualizer/backend/tests/test_neurons.py @@ -4,7 +4,6 @@ from pytest_unordered import unordered from api.models import ( - Dataset as DatasetModel, Neuron as NeuronModel, Connection as ConnectionModel, ) @@ -13,21 +12,6 @@ # Some test data -datasets = [ - { - "id": "ds1", - "name": "Dataset 1", - }, - { - "id": "ds2", - "name": "Gamma Goblin", - }, - { - "id": "ds3", - "name": "Dr. Seuss", - }, -] - neurons = [ { "name": "ADAL", @@ -67,24 +51,39 @@ }, ] -connections = lambda: [ +datasets = [ + { + "id": "ds1", + "name": "Dataset 1", + }, + { + "id": "ds2", + "name": "Gamma Goblin", + }, + { + "id": "ds3", + "name": "Dr. Seuss", + }, +] + +connections = [ { - "dataset": DatasetModel.objects.get(id="ds1"), + "dataset": datasets[0], "pre": "ADAL", "post": "ADAR", }, { - "dataset": DatasetModel.objects.get(id="ds2"), + "dataset": datasets[1], "pre": "ADEL", "post": "ADER", }, { - "dataset": DatasetModel.objects.get(id="ds2"), + "dataset": datasets[1], "pre": "ADAR", "post": "ADEL", }, { - "dataset": DatasetModel.objects.get(id="ds3"), + "dataset": datasets[2], "pre": "ADFR", "post": "ADAR", }, @@ -98,9 +97,8 @@ @pytest.fixture(scope="module") def django_db_setup(django_db_setup, django_db_blocker): with django_db_blocker.unblock(): - generate_instance(DatasetModel, datasets) generate_instance(NeuronModel, neurons) - generate_instance(ConnectionModel, connections()) + generate_instance(ConnectionModel, connections) # Fixture to access the test client in all test functions @@ -125,6 +123,8 @@ def test__get_all_cells(api_client): assert response.status_code == 200 neurons = response.json()["items"] + assert len(neurons) == len(expected_dataset_ids) + for neuron in neurons: name = neuron["name"] @@ -154,6 +154,9 @@ def test__get_all_cells_from_specific_datasets(api_client): assert response.status_code == 200 neurons = response.json()["items"] + + assert len(neurons) == len(expected_dataset_ids) + for neuron in neurons: name = neuron["name"] @@ -174,12 +177,11 @@ def test__search_cells(api_client): expected_neurons_names ), f"expected to query {len(expected_neurons_names)} and got {len(neurons)}" - for neuron in neurons: - assert neuron["name"] in expected_neurons_names + assert [n["name"] for n in neurons] == expected_neurons_names @pytest.mark.django_db # required to access the DB -def test__search_cells_in_datasets(api_client): +def test__search_cells_in_datasets_without_match(api_client): search_query = "ade" dataset_ids = ["ds1", "ds3"] @@ -193,22 +195,23 @@ def test__search_cells_in_datasets(api_client): neurons = response.json() assert len(neurons) == 0, f"expected datasets to not contain search matches" + +@pytest.mark.django_db # required to access the DB +def test__search_cells_in_datasets_with_match(api_client): + dataset_ids = ["ds1", "ds3", "ds2"] + search_query = "ade" + # Search dataset with matching neurons - dataset_ids.append("ds2") expected_neurons_names = ["ADEL", "ADER"] query_params = f"?name={search_query}&" + "&".join( [f"dataset_ids={ds}" for ds in dataset_ids] ) - print(query_params) response = api_client.get(f"/cells/search" + query_params) assert response.status_code == 200 neurons = response.json() - assert len(neurons) == len( - expected_neurons_names - ), f"expected to query {len(expected_neurons_names)} and got {len(neurons)}" + assert len(neurons) == len(expected_neurons_names) - for neuron in neurons: - assert neuron["name"] in expected_neurons_names + assert [n["name"] for n in neurons] == expected_neurons_names diff --git a/applications/visualizer/frontend/package-lock.json b/applications/visualizer/frontend/package-lock.json new file mode 100644 index 00000000..a6158640 --- /dev/null +++ b/applications/visualizer/frontend/package-lock.json @@ -0,0 +1,4587 @@ +{ + "name": "c-elegans-app", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "c-elegans-app", + "version": "0.0.1", + "dependencies": { + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.5", + "@metacell/geppetto-meta-client": "^1.2.8", + "@metacell/geppetto-meta-core": "^1.2.8", + "@metacell/geppetto-meta-ui": "^1.2.8", + "@mui/base": "^5.0.0-beta.40", + "@mui/icons-material": "^5.16.4", + "@mui/material": "^5.16.4", + "@react-three/drei": "^9.105.4", + "@react-three/fiber": "^8.16.2", + "@reduxjs/toolkit": "^2.2.3", + "@types/cytoscape": "^3.21.1", + "@types/three": "^0.166.0", + "cytoscape": "^3.29.2", + "cytoscape-dagre": "^2.5.0", + "cytoscape-fcose": "^2.2.0", + "file-saver": "^2.0.5", + "ol": "^9.1.0", + "react": "^18.3.1", + "react-color": "^2.19.3", + "react-dom": "^18.3.1", + "react-redux": "^9.1.1", + "three": "^0.166.1" + }, + "devDependencies": { + "@biomejs/biome": "1.8.3", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.2.1", + "openapi-typescript-codegen": "^0.29.0", + "sass": "^1.75.0", + "typescript": "^5.2.2", + "vite": "^5.4.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.5.5", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.5.5.tgz", + "integrity": "sha512-hv/aXDILyroHioVW27etFMV+IX6FyNn41YwbeGIAt5h/7fUTQvHI5w3ols8qYAT8aQt3kzexq5ZwxFDxNHIhdQ==", + "dev": true, + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "dependencies": { + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", + "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", + "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.4", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.24.4", + "@babel/parser": "^7.24.4", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.1", + "@babel/types": "^7.24.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz", + "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "dependencies": { + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.4.tgz", + "integrity": "sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.1", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", + "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.1.tgz", + "integrity": "sha512-kDJgnPujTmAZ/9q2CN4m2/lRsUUPDvsG3+tSHWUJIzMGTt5U/b/fwWd3RO3n+5mjLrsBrVa5eKFRVSQbi3dF1w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.1.tgz", + "integrity": "sha512-1v202n7aUq4uXAieRTKcwPzNyphlCuqHHDcdSNc+vdhoTEZcFMh+L5yZuCmGaIO7bs1nJUNfHB89TZyoL48xNA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz", + "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", + "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.1", + "@babel/generator": "^7.24.1", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.24.1", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@biomejs/biome": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.8.3.tgz", + "integrity": "sha512-/uUV3MV+vyAczO+vKrPdOW0Iaet7UnJMU4bNMinggGJTAnBPjCoLEYcyYtYHNnUNYlv4xZMH6hVIQCAozq8d5w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "1.8.3", + "@biomejs/cli-darwin-x64": "1.8.3", + "@biomejs/cli-linux-arm64": "1.8.3", + "@biomejs/cli-linux-arm64-musl": "1.8.3", + "@biomejs/cli-linux-x64": "1.8.3", + "@biomejs/cli-linux-x64-musl": "1.8.3", + "@biomejs/cli-win32-arm64": "1.8.3", + "@biomejs/cli-win32-x64": "1.8.3" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.8.3.tgz", + "integrity": "sha512-9DYOjclFpKrH/m1Oz75SSExR8VKvNSSsLnVIqdnKexj6NwmiMlKk94Wa1kZEdv6MCOHGHgyyoV57Cw8WzL5n3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.8.3.tgz", + "integrity": "sha512-UeW44L/AtbmOF7KXLCoM+9PSgPo0IDcyEUfIoOXYeANaNXXf9mLUwV1GeF2OWjyic5zj6CnAJ9uzk2LT3v/wAw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.8.3.tgz", + "integrity": "sha512-fed2ji8s+I/m8upWpTJGanqiJ0rnlHOK3DdxsyVLZQ8ClY6qLuPc9uehCREBifRJLl/iJyQpHIRufLDeotsPtw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.8.3.tgz", + "integrity": "sha512-9yjUfOFN7wrYsXt/T/gEWfvVxKlnh3yBpnScw98IF+oOeCYb5/b/+K7YNqKROV2i1DlMjg9g/EcN9wvj+NkMuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.8.3.tgz", + "integrity": "sha512-I8G2QmuE1teISyT8ie1HXsjFRz9L1m5n83U1O6m30Kw+kPMPSKjag6QGUn+sXT8V+XWIZxFFBoTDEDZW2KPDDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.8.3.tgz", + "integrity": "sha512-UHrGJX7PrKMKzPGoEsooKC9jXJMa28TUSMjcIlbDnIO4EAavCoVmNQaIuUSH0Ls2mpGMwUIf+aZJv657zfWWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.8.3.tgz", + "integrity": "sha512-J+Hu9WvrBevfy06eU1Na0lpc7uR9tibm9maHynLIoAjLZpQU3IW+OKHUtyL8p6/3pT2Ju5t5emReeIS2SAxhkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.8.3.tgz", + "integrity": "sha512-/PJ59vA1pnQeKahemaQf4Nyj7IKUvGQSc3Ze1uIGi+Wvr1xF7rGobSrAAG01T/gUDG21vkDsZYM03NAmPiVkqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/react": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", + "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", + "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", + "dependencies": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, + "node_modules/@emotion/styled": { + "version": "11.11.5", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.5.tgz", + "integrity": "sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/is-prop-valid": "^1.2.2", + "@emotion/serialize": "^1.1.4", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "dependencies": { + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", + "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", + "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", + "dependencies": { + "@floating-ui/dom": "^1.6.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, + "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/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true + }, + "node_modules/@material-ui/core": { + "version": "4.12.4", + "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.4.tgz", + "integrity": "sha512-tr7xekNlM9LjA6pagJmL8QCgZXaubWUwkJnoYcMKd4gw/t4XiyvnTkjdGrUVicyB2BsdaAv1tvow45bPM4sSwQ==", + "deprecated": "Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.", + "dependencies": { + "@babel/runtime": "^7.4.4", + "@material-ui/styles": "^4.11.5", + "@material-ui/system": "^4.12.2", + "@material-ui/types": "5.1.0", + "@material-ui/utils": "^4.11.3", + "@types/react-transition-group": "^4.2.0", + "clsx": "^1.0.4", + "hoist-non-react-statics": "^3.3.2", + "popper.js": "1.16.1-lts", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0", + "react-transition-group": "^4.4.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/material-ui" + }, + "peerDependencies": { + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/styles": { + "version": "4.11.5", + "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.5.tgz", + "integrity": "sha512-o/41ot5JJiUsIETME9wVLAJrmIWL3j0R0Bj2kCOLbSfqEkKf0fmaPt+5vtblUh5eXr2S+J/8J3DaCb10+CzPGA==", + "deprecated": "Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.", + "dependencies": { + "@babel/runtime": "^7.4.4", + "@emotion/hash": "^0.8.0", + "@material-ui/types": "5.1.0", + "@material-ui/utils": "^4.11.3", + "clsx": "^1.0.4", + "csstype": "^2.5.2", + "hoist-non-react-statics": "^3.3.2", + "jss": "^10.5.1", + "jss-plugin-camel-case": "^10.5.1", + "jss-plugin-default-unit": "^10.5.1", + "jss-plugin-global": "^10.5.1", + "jss-plugin-nested": "^10.5.1", + "jss-plugin-props-sort": "^10.5.1", + "jss-plugin-rule-value-function": "^10.5.1", + "jss-plugin-vendor-prefixer": "^10.5.1", + "prop-types": "^15.7.2" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/material-ui" + }, + "peerDependencies": { + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/styles/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/@material-ui/styles/node_modules/csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==" + }, + "node_modules/@material-ui/system": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.12.2.tgz", + "integrity": "sha512-6CSKu2MtmiJgcCGf6nBQpM8fLkuB9F55EKfbdTC80NND5wpTmKzwdhLYLH3zL4cLlK0gVaaltW7/wMuyTnN0Lw==", + "dependencies": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.3", + "csstype": "^2.5.2", + "prop-types": "^15.7.2" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/material-ui" + }, + "peerDependencies": { + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/system/node_modules/csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==" + }, + "node_modules/@material-ui/types": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", + "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==", + "peerDependencies": { + "@types/react": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/utils": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.3.tgz", + "integrity": "sha512-ZuQPV4rBK/V1j2dIkSSEcH5uT6AaHuKWFfotADHsC0wVL1NLd2WkFCm4ZZbX33iO4ydl6V0GPngKm8HZQ2oujg==", + "dependencies": { + "@babel/runtime": "^7.4.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + } + }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.8.tgz", + "integrity": "sha512-Rp7ll8BHrKB3wXaRFKhrltwZl1CiXGdibPxuWXvqGnKTnv8fqa/nvftYNuSbf+pbJWKYCXdBtYTITdAUTGGh0Q==" + }, + "node_modules/@metacell/geppetto-meta-client": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@metacell/geppetto-meta-client/-/geppetto-meta-client-1.2.8.tgz", + "integrity": "sha512-pDORhlNYv5HILPaGPuJGFV5xLgvdtrtHklBfTW3jMfBAzbXO5qxRyslfpES2VxdcFGONW5Gr6KCbRWBmm2My+g==", + "dependencies": { + "@material-ui/core": "^4.1.3", + "pako": "^1.0.3", + "react": "^17.0.2", + "react-redux": "^7.2.3", + "react-rnd": "^7.3.0", + "redux": "^4.1.0", + "url-join": "^4.0.0" + } + }, + "node_modules/@metacell/geppetto-meta-client/node_modules/react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@metacell/geppetto-meta-client/node_modules/react-redux": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@metacell/geppetto-meta-core": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@metacell/geppetto-meta-core/-/geppetto-meta-core-1.2.8.tgz", + "integrity": "sha512-1AMhQLw3+3YFufG2YUsgsxi3YGZx94VKloXq5aENZo5ZLW+aLvlSAUcRUl68LUPt/Axd+6xc1R7qGNOghv7Irw==" + }, + "node_modules/@metacell/geppetto-meta-ui": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@metacell/geppetto-meta-ui/-/geppetto-meta-ui-1.2.8.tgz", + "integrity": "sha512-wiC7GiJZWp66wUG4jIGcP1wieAi4fEtyncpEcNjwssuURSddixFnCkH0dSX5KwJ3PjDbHEyco6/M6nG+DYFJlw==", + "peerDependencies": { + "@fortawesome/fontawesome-free": "^6.4.0", + "@fortawesome/fontawesome-svg-core": "^1.2.35", + "@fortawesome/free-solid-svg-icons": "^5.13.0", + "@fortawesome/react-fontawesome": "^0.1.9", + "@material-ui/core": "4.11.4", + "@material-ui/icons": "^4.11.2", + "@metacell/geppetto-meta-core": "1.2.8", + "aframe": "<1.1.0", + "aframe-slice9-component": ">=1.0.0", + "ami.js": ">=0.32.0", + "axios": "^0.21.1", + "babel-plugin-import-less": "^0.1.6", + "d3": "7.6.1", + "d3-plugins-dist": "^3.2.0", + "file-saver": "^2.0.2", + "griddle-react": "^1.13.1", + "html-to-image": "^1.9.0", + "jszip": "^3.2.1", + "mathjs": "^3.5.3", + "openseadragon": "^2.2.1", + "plotly.js": "^1.42.5", + "react": "^17.0.2", + "react-color": "^2.17.3", + "react-dom": "^17.0.2", + "react-dom-factories": "^1.0.2", + "react-fontawesome": "^1.6.1", + "react-force-graph-2d": ">=1.23.8", + "react-force-graph-3d": ">=1.21.10", + "react-jsonschema-form": "^1.0.6", + "react-lazy-load": "^3.0.13", + "react-modal": "^3.8.1", + "react-overlays": "^0.8.0", + "react-player": "^0.18.0", + "react-plotly.js": "^2.3.0", + "react-resize-detector": "^6.7.4", + "react-rnd": "^7.3.0", + "react-router": "^4.2.0", + "react-router-dom": "^4.2.0", + "react-slick": "^0.29.0", + "three": ">=0.111.0", + "three-render-objects": ">=1.13.3", + "underscore": "~1.9.1" + } + }, + "node_modules/@monogrid/gainmap-js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.0.5.tgz", + "integrity": "sha512-53sCTG4FaJBaAq/tcufARtVYDMDGqyBT9i7F453pWGhZ5LqubDHDWtYoHo9VhQqMcHTEexdJqSsR58y+9HVmQA==", + "dependencies": { + "promise-worker-transferable": "^1.0.4" + }, + "peerDependencies": { + "three": ">= 0.159.0" + } + }, + "node_modules/@mui/base": { + "version": "5.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", + "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@floating-ui/react-dom": "^2.0.8", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "@popperjs/core": "^2.11.8", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/base/node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.4.tgz", + "integrity": "sha512-rNdHXhclwjEZnK+//3SR43YRx0VtjdHnUFhMSGYmAMJve+KiwEja/41EYh8V3pZKqF2geKyfcFUenTfDTYUR4w==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.4.tgz", + "integrity": "sha512-j9/CWctv6TH6Dou2uR2EH7UOgu79CW/YcozxCYVLJ7l03pCsiOlJ5sBArnWJxJ+nGkFwyL/1d1k8JEPMDR125A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.4.tgz", + "integrity": "sha512-dBnh3/zRYgEVIS3OE4oTbujse3gifA0qLMmuUk13ywsDCbngJsdgwW5LuYeiT5pfA8PGPGSqM7mxNytYXgiMCw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/core-downloads-tracker": "^5.16.4", + "@mui/system": "^5.16.4", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.4", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^18.3.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@mui/material/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==", + "license": "MIT" + }, + "node_modules/@mui/private-theming": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.4.tgz", + "integrity": "sha512-ZsAm8cq31SJ37SVWLRlu02v9SRthxnfQofaiv14L5Bht51B0dz6yQEoVU/V8UduZDCCIrWkBHuReVfKhE/UuXA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.16.4", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.4.tgz", + "integrity": "sha512-0+mnkf+UiAmTVB8PZFqOhqf729Yh0Cxq29/5cA3VAyDVTRIUUQ8FXQhiAhUIbijFmM72rY80ahFPXIm4WDbzcA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@emotion/cache": "^11.11.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.4.tgz", + "integrity": "sha512-ET1Ujl2/8hbsD611/mqUuNArMCGv/fIWO/f8B3ZqF5iyPHM2aS74vhTNyjytncc4i6dYwGxNk+tLa7GwjNS0/w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.16.4", + "@mui/styled-engine": "^5.16.4", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.4", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/system/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@mui/types": { + "version": "7.2.15", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.15.tgz", + "integrity": "sha512-nbo7yPhtKJkdf9kcVOF8JZHPZTmqXjJ/tI0bdWgHg5tp9AnIN4Y7f7wm9T+0SyGYJk76+GYZ8Q5XaTYAsUHN0Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.4.tgz", + "integrity": "sha512-nlppYwq10TBIFqp7qxY0SvbACOXeOjeVL3pOcDsK0FT8XjrEXh9/+lkg8AEIzD16z7YfiJDQjaJG2OLkE7BxNg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^18.3.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@mui/utils/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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@react-spring/animated": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.6.1.tgz", + "integrity": "sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==", + "dependencies": { + "@react-spring/shared": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.6.1.tgz", + "integrity": "sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==", + "dependencies": { + "@react-spring/animated": "~9.6.1", + "@react-spring/rafz": "~9.6.1", + "@react-spring/shared": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.6.1.tgz", + "integrity": "sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ==" + }, + "node_modules/@react-spring/shared": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.6.1.tgz", + "integrity": "sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==", + "dependencies": { + "@react-spring/rafz": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/three": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.6.1.tgz", + "integrity": "sha512-Tyw2YhZPKJAX3t2FcqvpLRb71CyTe1GvT3V+i+xJzfALgpk10uPGdGaQQ5Xrzmok1340DAeg2pR/MCfaW7b8AA==", + "dependencies": { + "@react-spring/animated": "~9.6.1", + "@react-spring/core": "~9.6.1", + "@react-spring/shared": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "peerDependencies": { + "@react-three/fiber": ">=6.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "three": ">=0.126" + } + }, + "node_modules/@react-spring/types": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.6.1.tgz", + "integrity": "sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q==" + }, + "node_modules/@react-three/drei": { + "version": "9.105.4", + "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-9.105.4.tgz", + "integrity": "sha512-pBZQmaV4yuBXP/TMcWJc5RNm3v9CykOqQDg2tyjZHijV4aa8jf38ae7WyQa5zDjuZcrHlQd2IGMX0Ia2UTHEUA==", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@mediapipe/tasks-vision": "0.10.8", + "@monogrid/gainmap-js": "^3.0.5", + "@react-spring/three": "~9.6.1", + "@use-gesture/react": "^10.2.24", + "camera-controls": "^2.4.2", + "cross-env": "^7.0.3", + "detect-gpu": "^5.0.28", + "glsl-noise": "^0.0.0", + "hls.js": "1.3.5", + "maath": "^0.10.7", + "meshline": "^3.1.6", + "react-composer": "^5.0.3", + "stats-gl": "^2.0.0", + "stats.js": "^0.17.0", + "suspend-react": "^0.1.3", + "three-mesh-bvh": "^0.7.0", + "three-stdlib": "^2.29.4", + "troika-three-text": "^0.49.0", + "tunnel-rat": "^0.1.2", + "utility-types": "^3.10.0", + "uuid": "^9.0.1", + "zustand": "^3.7.1" + }, + "peerDependencies": { + "@react-three/fiber": ">=8.0", + "react": ">=18.0", + "react-dom": ">=18.0", + "three": ">=0.137" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/@react-three/fiber": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.16.2.tgz", + "integrity": "sha512-3Z5FW8mxzomBbrw2iF0lNOAlNBr2OK6HR0NM416PzcTs0UcSoPj/nD4eqmrV5Kut6kvCc/TJua5LyeoPE7vSmw==", + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/react-reconciler": "^0.26.7", + "@types/webxr": "*", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "its-fine": "^1.0.6", + "react-reconciler": "^0.27.0", + "react-use-measure": "^2.1.1", + "scheduler": "^0.21.0", + "suspend-react": "^0.1.3", + "zustand": "^3.7.1" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-file-system": ">=11.0", + "expo-gl": ">=11.0", + "react": ">=18.0", + "react-dom": ">=18.0", + "react-native": ">=0.64", + "three": ">=0.133" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-file-system": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@react-three/fiber/node_modules/scheduler": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", + "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.3.tgz", + "integrity": "sha512-76dll9EnJXg4EVcI5YNxZA/9hSAmZsFqzMmNRHvIlzw2WS/twfcVX3ysYrWGJMClwEmChQFC4yRq74tn6fdzRA==", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.0.1" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.3.tgz", + "integrity": "sha512-X9alQ3XM6I9IlSlmC8ddAvMSyG1WuHk5oUnXGw+yUBs3BFoTizmG1La/Gr8fVJvDWAq+zlYTZ9DBgrlKRVY06g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.3.tgz", + "integrity": "sha512-eQK5JIi+POhFpzk+LnjKIy4Ks+pwJ+NXmPxOCSvOKSNRPONzKuUvWE+P9JxGZVxrtzm6BAYMaL50FFuPe0oWMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.3.tgz", + "integrity": "sha512-Od4vE6f6CTT53yM1jgcLqNfItTsLt5zE46fdPaEmeFHvPs5SjZYlLpHrSiHEKR1+HdRfxuzXHjDOIxQyC3ptBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.3.tgz", + "integrity": "sha512-0IMAO21axJeNIrvS9lSe/PGthc8ZUS+zC53O0VhF5gMxfmcKAP4ESkKOCwEi6u2asUrt4mQv2rjY8QseIEb1aw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.3.tgz", + "integrity": "sha512-ge2DC7tHRHa3caVEoSbPRJpq7azhG+xYsd6u2MEnJ6XzPSzQsTKyXvh6iWjXRf7Rt9ykIUWHtl0Uz3T6yXPpKw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.14.3.tgz", + "integrity": "sha512-ljcuiDI4V3ySuc7eSk4lQ9wU8J8r8KrOUvB2U+TtK0TiW6OFDmJ+DdIjjwZHIw9CNxzbmXY39wwpzYuFDwNXuw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.3.tgz", + "integrity": "sha512-Eci2us9VTHm1eSyn5/eEpaC7eP/mp5n46gTRB3Aar3BgSvDQGJZuicyq6TsH4HngNBgVqC5sDYxOzTExSU+NjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.3.tgz", + "integrity": "sha512-UrBoMLCq4E92/LCqlh+blpqMz5h1tJttPIniwUgOFJyjWI1qrtrDhhpHPuFxULlUmjFHfloWdixtDhSxJt5iKw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.3.tgz", + "integrity": "sha512-5aRjvsS8q1nWN8AoRfrq5+9IflC3P1leMoy4r2WjXyFqf3qcqsxRCfxtZIV58tCxd+Yv7WELPcO9mY9aeQyAmw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.3.tgz", + "integrity": "sha512-sk/Qh1j2/RJSX7FhEpJn8n0ndxy/uf0kI/9Zc4b1ELhqULVdTfN6HL31CDaTChiBAOgLcsJ1sgVZjWv8XNEsAQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.3.tgz", + "integrity": "sha512-jOO/PEaDitOmY9TgkxF/TQIjXySQe5KVYB57H/8LRP/ux0ZoO8cSHCX17asMSv3ruwslXW/TLBcxyaUzGRHcqg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.3.tgz", + "integrity": "sha512-8ybV4Xjy59xLMyWo3GCfEGqtKV5M5gCSrZlxkPGvEPCGDLNla7v48S662HSGwRd6/2cSneMQWiv+QzcttLrrOA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.3.tgz", + "integrity": "sha512-s+xf1I46trOY10OqAtZ5Rm6lzHre/UiLA1J2uOhCFXWkbZrJRkYBPO6FhvGfHmdtQ3Bx793MNa7LvoWFAm93bg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.3.tgz", + "integrity": "sha512-+4h2WrGOYsOumDQ5S2sYNyhVfrue+9tc9XcLWLh+Kw3UOxAvrfOrSMFon60KspcDdytkNDh7K2Vs6eMaYImAZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.3.tgz", + "integrity": "sha512-T1l7y/bCeL/kUwh9OD4PQT4aM7Bq43vX05htPJJ46RTI4r5KNt6qJRzAfNfM+OYMNEVBWQzR2Gyk+FXLZfogGw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.3.tgz", + "integrity": "sha512-/BypzV0H1y1HzgYpxqRaXGBRqfodgoBBCcsrujT6QRcakDQdfU+Lq9PENPh5jB4I44YWq+0C2eHsHya+nZY1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.2", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.2.tgz", + "integrity": "sha512-kMCNaZCJugWI86xiEHaY338CU5JpD0B97p1j1IKNn/Zto8PgACjQx0UxbHjmOcLl/dDOBnItwD07KmCs75pxtQ==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/cytoscape": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.21.1.tgz", + "integrity": "sha512-vBC2w0ciULoay50QnSScFg9Yu/9gimyor3vb4b4gEEI+4Ccfu/AH7gA+YKAcFIvo1mgKVhXaewNxw3zC80cXoA==" + }, + "node_modules/@types/draco3d": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.9.tgz", + "integrity": "sha512-4MMUjMQb4yA5fJ4osXx+QxGHt0/ZSy4spT6jL1HM7Tn8OJEC35siqdnpOo+HxPhYjqEFumKfGVF9hJfdyKBIBA==" + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==" + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-reconciler": { + "version": "0.26.7", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.26.7.tgz", + "integrity": "sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-redux": { + "version": "7.1.33", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", + "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/stats.js": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz", + "integrity": "sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==" + }, + "node_modules/@types/three": { + "version": "0.166.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.166.0.tgz", + "integrity": "sha512-FHMnpcdhdbdOOIYbfkTkUVpYMW53odxbTRwd0/xJpYnTzEsjnVnondGAvHZb4z06UW0vo6WPVuvH0/9qrxKx7g==", + "license": "MIT", + "dependencies": { + "@tweenjs/tween.js": "~23.1.2", + "@types/stats.js": "*", + "@types/webxr": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.18.1" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, + "node_modules/@types/webxr": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.15.tgz", + "integrity": "sha512-nC9116Gd4N+CqTxqo6gvCfhAMAzgRcfS8ZsciNodHq8uwW4JCVKwhagw8yN0XmC7mHrLnWqniJpoVEiR+72Drw==" + }, + "node_modules/@use-gesture/core": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", + "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==" + }, + "node_modules/@use-gesture/react": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", + "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", + "dependencies": { + "@use-gesture/core": "10.3.1" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz", + "integrity": "sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.5", + "@babel/plugin-transform-react-jsx-self": "^7.23.3", + "@babel/plugin-transform-react-jsx-source": "^7.23.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camera-controls": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.8.3.tgz", + "integrity": "sha512-zFjqUR6onLkG+z1A6vAWfzovxZxWVSvp6e5t3lfZgfgPZtX3n74aykNAUaoRbq8Y3tOxadHkDjbfGDOP9hFf2w==", + "peerDependencies": { + "three": ">=0.126.1" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001611", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001611.tgz", + "integrity": "sha512-19NuN1/3PjA3QI8Eki55N8my4LzfkMCRLgCVfrl/slbSAchQfV0+GwjPrK3rq37As4UCLlM/DHajbKkAqbv92Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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/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/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/color-parse": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/color-parse/-/color-parse-2.0.2.tgz", + "integrity": "sha512-eCtOz5w5ttWIUcaKLiktF+DxZO1R9KLNY/xhbV6CkhM7sR3GhVghmt6X6yOnzeaM24po+Z9/S1apbXMwA3Iepw==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + } + }, + "node_modules/color-parse/node_modules/color-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.0.tgz", + "integrity": "sha512-SbtvAMWvASO5TE2QP07jHBMXKafgdZz8Vrsrn96fiL+O92/FN/PLARzUW5sKt013fjAprK2d2iCn2hk2Xb5oow==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color-rgba": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color-rgba/-/color-rgba-3.0.0.tgz", + "integrity": "sha512-PPwZYkEY3M2THEHHV6Y95sGUie77S7X8v+h1r6LSAPF3/LL2xJ8duUXSrkic31Nzc4odPwHgUbiX/XuTYzQHQg==", + "license": "MIT", + "dependencies": { + "color-parse": "^2.0.0", + "color-space": "^2.0.0" + } + }, + "node_modules/color-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-space/-/color-space-2.0.1.tgz", + "integrity": "sha512-nKqUYlo0vZATVOFHY810BSYjmCARrG7e5R3UE3CQlyjJTvv5kSSmPG1kzm/oDyyqjehM+lW1RnEt9It9GNa5JA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", + "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-vendor": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", + "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", + "dependencies": { + "@babel/runtime": "^7.8.3", + "is-in-browser": "^1.0.2" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/cytoscape": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.29.2.tgz", + "integrity": "sha512-2G1ycU28Nh7OHT9rkXRLpCDP30MKH1dXJORZuBhtEhEW7pKwgPi77ImqlCWinouyE1PNepIOGZBOrE84DG7LyQ==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-dagre": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cytoscape-dagre/-/cytoscape-dagre-2.5.0.tgz", + "integrity": "sha512-VG2Knemmshop4kh5fpLO27rYcyUaaDkRw+6PiX4bstpB+QFt0p2oauMrsjVbUamGWQ6YNavh7x2em2uZlzV44g==", + "dependencies": { + "dagre": "^0.8.5" + }, + "peerDependencies": { + "cytoscape": "^3.2.22" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-gpu": { + "version": "5.0.38", + "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.38.tgz", + "integrity": "sha512-36QeGHSXYcJ/RfrnPEScR8GDprbXFG4ZhXsfVNVHztZr38+fRxgHnJl3CjYXXjbeRUhu3ZZBJh6Lg0A9v0Qd8A==", + "dependencies": { + "webgl-constants": "^1.1.1" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==" + }, + "node_modules/earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", + "license": "ISC" + }, + "node_modules/electron-to-chromium": { + "version": "1.4.742", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.742.tgz", + "integrity": "sha512-EhE+z1d5RNytAq/qnGAxPR+ie3UzKbv7qqQc0wnEbOh+KDUplgfzkGSCy9d78B+S+nVNTS42BabHXB6Ni+Ud4w==", + "dev": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" + }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "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==", + "license": "MIT", + "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/geotiff/node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/glsl-noise": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", + "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hls.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.3.5.tgz", + "integrity": "sha512-uybAvKS6uDe0MnWNEPnO0krWVr+8m2R0hJ/viql8H3MVK+itq8gGQuIYoFHL3rECkIpNH98Lw8YuuWMKZxp3Ew==" + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/hyphenate-style-name": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", + "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "node_modules/immer": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.4.tgz", + "integrity": "sha512-cuBuGK40P/sk5IzWa9QPUaAdvPHjkk1c+xYsd9oZw+YQQEV+10G0P5uMpGctZZKnyQ+ibRO08bD25nWLmYi2pw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/immutable": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", + "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-in-browser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", + "integrity": "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/its-fine": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz", + "integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==", + "dependencies": { + "@types/react-reconciler": "^0.28.0" + }, + "peerDependencies": { + "react": ">=18.0" + } + }, + "node_modules/its-fine/node_modules/@types/react-reconciler": { + "version": "0.28.8", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.8.tgz", + "integrity": "sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jss": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss/-/jss-10.10.0.tgz", + "integrity": "sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "csstype": "^3.0.2", + "is-in-browser": "^1.1.3", + "tiny-warning": "^1.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/jss" + } + }, + "node_modules/jss-plugin-camel-case": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.10.0.tgz", + "integrity": "sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "hyphenate-style-name": "^1.0.3", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-default-unit": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.10.0.tgz", + "integrity": "sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-global": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.10.0.tgz", + "integrity": "sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-nested": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.10.0.tgz", + "integrity": "sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-props-sort": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.10.0.tgz", + "integrity": "sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-rule-value-function": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.10.0.tgz", + "integrity": "sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-vendor-prefixer": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.10.0.tgz", + "integrity": "sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "css-vendor": "^2.0.8", + "jss": "10.10.0" + } + }, + "node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==" + }, + "node_modules/lerc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz", + "integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==", + "license": "Apache-2.0" + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/lodash": { + "version": "4.17.21", + "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/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/maath": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.7.tgz", + "integrity": "sha512-zQ2xd7dNOIVTjAS+hj22fyj1EFYmOJX6tzKjZ92r6WDoq8hyFxjuGA2q950tmR4iC/EKXoMQdSipkaJVuUHDTg==", + "peerDependencies": { + "@types/three": ">=0.144.0", + "three": ">=0.144.0" + } + }, + "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/meshline": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.0.tgz", + "integrity": "sha512-EKKf2TLnfyqUeA7ryWFKgT9HchTMATvECGZnMQjtlcyxK0sB8shVLVkemBUp9dB3tkDEmoqQDLJCPStjkH8D7A==", + "peerDependencies": { + "three": ">=0.137" + } + }, + "node_modules/meshoptimizer": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", + "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==" + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ol": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/ol/-/ol-9.2.4.tgz", + "integrity": "sha512-bsbu4ObaAlbELMIZWnYEvX4Z9jO+OyCBshtODhDKmqYTPEfnKOX3RieCr97tpJkqWTZvyV4tS9UQDvHoCdxS+A==", + "license": "BSD-2-Clause", + "dependencies": { + "color-rgba": "^3.0.0", + "color-space": "^2.0.1", + "earcut": "^2.2.3", + "geotiff": "^2.0.7", + "pbf": "3.2.1", + "rbush": "^3.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/openlayers" + } + }, + "node_modules/openapi-typescript-codegen": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/openapi-typescript-codegen/-/openapi-typescript-codegen-0.29.0.tgz", + "integrity": "sha512-/wC42PkD0LGjDTEULa/XiWQbv4E9NwLjwLjsaJ/62yOsoYhwvmBR31kPttn1DzQ2OlGe5stACcF/EIkZk43M6w==", + "dev": true, + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.5.4", + "camelcase": "^6.3.0", + "commander": "^12.0.0", + "fs-extra": "^11.2.0", + "handlebars": "^4.7.8" + }, + "bin": { + "openapi": "bin/index.js" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "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==", + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/pbf": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", + "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==", + "license": "BSD-3-Clause", + "dependencies": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/popper.js": { + "version": "1.16.1-lts", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", + "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==" + }, + "node_modules/postcss": { + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==" + }, + "node_modules/promise-worker-transferable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", + "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==", + "dependencies": { + "is-promise": "^2.1.0", + "lie": "^3.0.2" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "license": "ISC" + }, + "node_modules/rbush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", + "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", + "license": "MIT", + "dependencies": { + "quickselect": "^2.0.0" + } + }, + "node_modules/re-resizable": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-4.5.1.tgz", + "integrity": "sha512-amjlp4IuTSHs4XG1bP5WbAgBDIZitODKIsqcpZsNhEBYYEidol0dlP4S9zHiN3iu6Tff4WfYuruihLgN7RJeQw==" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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-composer": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/react-composer/-/react-composer-5.0.3.tgz", + "integrity": "sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==", + "dependencies": { + "prop-types": "^15.6.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-draggable": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-3.3.2.tgz", + "integrity": "sha512-oaz8a6enjbPtx5qb0oDWxtDNuybOylvto1QLydsXgKmwT7e3GXC2eMVDwEMIUYJIFqVG72XpOv673UuuAq6LhA==", + "dependencies": { + "classnames": "^2.2.5", + "prop-types": "^15.6.0" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "node_modules/react-reconciler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz", + "integrity": "sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.21.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/react-reconciler/node_modules/scheduler": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", + "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/react-redux": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.1.tgz", + "integrity": "sha512-5ynfGDzxxsoV73+4czQM56qF43vsmgJsO22rmAvU5tZT2z5Xow/A2uhhxwXuGTxgdReF3zcp7A80gma2onRs1A==", + "dependencies": { + "@types/use-sync-external-store": "^0.0.3", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25", + "react": "^18.0", + "react-native": ">=0.69", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", + "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-rnd": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-7.4.3.tgz", + "integrity": "sha512-TLQ35nqXup7rC63qAETebbO6Znilr20CroTTeAdlYu8nvRSwB7BrmPKZhHB2GgeiSucOoeCyAA9pHPhbMpEd/Q==", + "dependencies": { + "re-resizable": "4.5.1", + "react-draggable": "^3.0.5" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/react-use-measure": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.1.tgz", + "integrity": "sha512-nocZhN26cproIiIduswYpV5y5lQpSQS1y/4KuvUCjSKmw7ZWIS/+g3aFnX3WdBkyuGUtTLif3UTqnLLhbDoQig==", + "dependencies": { + "debounce": "^1.2.1" + }, + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + } + }, + "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/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reselect": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.0.tgz", + "integrity": "sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "license": "MIT", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, + "node_modules/rollup": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.3.tgz", + "integrity": "sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.14.3", + "@rollup/rollup-android-arm64": "4.14.3", + "@rollup/rollup-darwin-arm64": "4.14.3", + "@rollup/rollup-darwin-x64": "4.14.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.14.3", + "@rollup/rollup-linux-arm-musleabihf": "4.14.3", + "@rollup/rollup-linux-arm64-gnu": "4.14.3", + "@rollup/rollup-linux-arm64-musl": "4.14.3", + "@rollup/rollup-linux-powerpc64le-gnu": "4.14.3", + "@rollup/rollup-linux-riscv64-gnu": "4.14.3", + "@rollup/rollup-linux-s390x-gnu": "4.14.3", + "@rollup/rollup-linux-x64-gnu": "4.14.3", + "@rollup/rollup-linux-x64-musl": "4.14.3", + "@rollup/rollup-win32-arm64-msvc": "4.14.3", + "@rollup/rollup-win32-ia32-msvc": "4.14.3", + "@rollup/rollup-win32-x64-msvc": "4.14.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/sass": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.75.0.tgz", + "integrity": "sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stats-gl": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.2.8.tgz", + "integrity": "sha512-94G5nZvduDmzxBS7K0lYnynYwreZpkknD8g5dZmU6mpwIhy3caCrjAm11Qm1cbyx7mqix7Fp00RkbsonzKWnoQ==", + "dependencies": { + "@types/three": "^0.163.0" + } + }, + "node_modules/stats-gl/node_modules/@types/three": { + "version": "0.163.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.163.0.tgz", + "integrity": "sha512-uIdDhsXRpQiBUkflBS/i1l3JX14fW6Ot9csed60nfbZNXHDTRsnV2xnTVwXcgbvTiboAR4IW+t+lTL5f1rqIqA==", + "dependencies": { + "@tweenjs/tween.js": "~23.1.1", + "@types/stats.js": "*", + "@types/webxr": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.18.1" + } + }, + "node_modules/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==" + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "peerDependencies": { + "react": ">=17.0" + } + }, + "node_modules/three": { + "version": "0.166.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.166.1.tgz", + "integrity": "sha512-LtuafkKHHzm61AQA1be2MAYIw1IjmhOUxhBa0prrLpEMWbV7ijvxCRHjSgHPGp2493wLBzwKV46tA9nivLEgKg==", + "license": "MIT" + }, + "node_modules/three-mesh-bvh": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.4.tgz", + "integrity": "sha512-flxe0A4uflTPR6elgq/Y8VrLoljDNS899i422SxQcU3EtMj6o8z4kZRyqZqGWzR0qMf1InTZzY1/0xZl/rnvVw==", + "peerDependencies": { + "three": ">= 0.151.0" + } + }, + "node_modules/three-stdlib": { + "version": "2.29.6", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.29.6.tgz", + "integrity": "sha512-nj9bHkzhhwfmqQcM/keC2RDb0bHhbw6bRXTy81ehzi8F1rtp6pJ5eS0/vl1Eg5RMFqXOMyxJ6sDHPoLU+IrVZg==", + "dependencies": { + "@types/draco3d": "^1.4.0", + "@types/offscreencanvas": "^2019.6.4", + "@types/webxr": "^0.5.2", + "draco3d": "^1.4.1", + "fflate": "^0.6.9", + "potpack": "^1.0.1" + }, + "peerDependencies": { + "three": ">=0.128.0" + } + }, + "node_modules/three-stdlib/node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, + "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/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/troika-three-text": { + "version": "0.49.1", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.49.1.tgz", + "integrity": "sha512-lXGWxgjJP9kw4i4Wh+0k0Q/7cRfS6iOME4knKht/KozPu9GcFA9NnNpRvehIhrUawq9B0ZRw+0oiFHgRO+4Wig==", + "dependencies": { + "bidi-js": "^1.0.2", + "troika-three-utils": "^0.49.0", + "troika-worker-utils": "^0.49.0", + "webgl-sdf-generator": "1.1.1" + }, + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-three-utils": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.49.0.tgz", + "integrity": "sha512-umitFL4cT+Fm/uONmaQEq4oZlyRHWwVClaS6ZrdcueRvwc2w+cpNQ47LlJKJswpqtMFWbEhOLy0TekmcPZOdYA==", + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-worker-utils": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.49.0.tgz", + "integrity": "sha512-1xZHoJrG0HFfCvT/iyN41DvI/nRykiBtHqFkGaGgJwq5iXfIZFBiPPEHFpPpgyKM3Oo5ITHXP5wM2TNQszYdVg==" + }, + "node_modules/tunnel-rat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", + "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", + "dependencies": { + "zustand": "^4.3.2" + } + }, + "node_modules/tunnel-rat/node_modules/zustand": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", + "integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "dev": true, + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" + }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz", + "integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.40", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "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==", + "license": "Apache-2.0" + }, + "node_modules/webgl-constants": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", + "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" + }, + "node_modules/webgl-sdf-generator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", + "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, + "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==", + "license": "CC0-1.0" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/zstddec": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.1.0.tgz", + "integrity": "sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==", + "license": "MIT AND BSD-3-Clause" + }, + "node_modules/zustand": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", + "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==", + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + } + } +} diff --git a/applications/visualizer/frontend/package.json b/applications/visualizer/frontend/package.json index 68dea0a4..36c763a4 100644 --- a/applications/visualizer/frontend/package.json +++ b/applications/visualizer/frontend/package.json @@ -31,18 +31,21 @@ "cytoscape-dagre": "^2.5.0", "cytoscape-fcose": "^2.2.0", "file-saver": "^2.0.5", + "html2canvas": "^1.4.1", "ol": "^9.1.0", + "pako": "^2.1.0", "react": "^18.3.1", "react-color": "^2.19.3", "react-dom": "^18.3.1", "react-redux": "^9.1.1", + "react-router-dom": "^6.26.2", "three": "^0.166.1" }, "devDependencies": { "@biomejs/biome": "1.8.3", + "@types/cytoscape": "^3.21.5", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "@types/cytoscape": "^3.21.5", "@types/three": "^0.166.0", "@vitejs/plugin-react": "^4.2.1", "openapi-typescript-codegen": "^0.29.0", diff --git a/applications/visualizer/frontend/src/App.tsx b/applications/visualizer/frontend/src/App.tsx index e85ea373..44799a37 100644 --- a/applications/visualizer/frontend/src/App.tsx +++ b/applications/visualizer/frontend/src/App.tsx @@ -4,12 +4,14 @@ import { Provider } from "react-redux"; import theme from "./theme/index.tsx"; import "./App.css"; import React from "react"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; import AppLauncher from "./components/AppLauncher.tsx"; import Layout from "./components/ViewerContainer/Layout.tsx"; import WorkspaceComponent from "./components/WorkspaceComponent.tsx"; import CompareWrapper from "./components/wrappers/Compare.tsx"; import DefaultWrapper from "./components/wrappers/Default.tsx"; import { useGlobalContext } from "./contexts/GlobalContext.tsx"; +import GlobalContextReloader from "./contexts/GlobalContextReloader.tsx"; import { ViewMode } from "./models"; function App() { @@ -44,19 +46,27 @@ function App() { }; return ( - <> + - {hasLaunched ? ( - - - {renderWorkspaces()} - - ) : ( - - )} + + + + {renderWorkspaces()} + + ) : ( + + ) + } + /> + } /> + - + ); } diff --git a/applications/visualizer/frontend/src/components/CreateNewWorkspaceDialog.tsx b/applications/visualizer/frontend/src/components/CreateNewWorkspaceDialog.tsx index 89c8155d..0810ee40 100644 --- a/applications/visualizer/frontend/src/components/CreateNewWorkspaceDialog.tsx +++ b/applications/visualizer/frontend/src/components/CreateNewWorkspaceDialog.tsx @@ -4,38 +4,47 @@ import { useCallback, useState } from "react"; import { v4 as uuidv4 } from "uuid"; import { useGlobalContext } from "../contexts/GlobalContext.tsx"; import { CaretIcon, CheckIcon, CloseIcon } from "../icons"; -import { type Dataset, type Neuron, NeuronsService } from "../rest"; +import { GlobalError } from "../models/Error.ts"; +import { type Dataset, NeuronsService } from "../rest"; import { vars as colors } from "../theme/variables.ts"; import CustomAutocomplete from "./CustomAutocomplete.tsx"; import CustomDialog from "./CustomDialog.tsx"; const CreateNewWorkspaceDialog = ({ onCloseCreateWorkspace, showCreateWorkspaceDialog, isCompareMode, title, subTitle, submitButtonText }) => { - const [neurons, setNeurons] = useState([]); - const { workspaces, datasets, createWorkspace, setSelectedWorkspacesIds } = useGlobalContext(); + const [neuronNames, setNeuronsNames] = useState([]); + const { workspaces, datasets, createWorkspace, setSelectedWorkspacesIds, handleErrors } = useGlobalContext(); const [searchedNeuron, setSearchedNeuron] = useState(""); const [formValues, setFormValues] = useState<{ workspaceName: string; selectedDatasets: Dataset[]; - selectedNeurons: Neuron[]; + selectedNeurons: string[]; }>({ workspaceName: "", selectedDatasets: [], selectedNeurons: [], }); - const [errorMessage, setErrorMessage] = useState(""); const workspaceFieldName = "workspaceName"; + const fetchNeurons = async (name, datasetsIds) => { try { const Ids = datasetsIds.map((dataset) => dataset.id); - const response = await NeuronsService.searchCells({ + const neuronArrays = await NeuronsService.searchCells({ name: name, datasetIds: Ids, }); - setNeurons(response); + + // We add the neurons classes + const uniqueNeurons = new Set(); + for (const neuron of neuronArrays.flat()) { + uniqueNeurons.add(neuron.name); + uniqueNeurons.add(neuron.nclass); + } + + setNeuronsNames([...uniqueNeurons]); } catch (error) { - console.error("Failed to fetch datasets", error); + handleErrors(new GlobalError(error.message)); } }; @@ -78,7 +87,7 @@ const CreateNewWorkspaceDialog = ({ onCloseCreateWorkspace, showCreateWorkspaceD const randomNumber = uuidv4().replace(/\D/g, "").substring(0, 13); const newWorkspaceId = `workspace-${randomNumber}`; - const activeNeurons = new Set(formValues.selectedNeurons.map((neuron) => neuron.name)); + const activeNeurons = new Set(formValues.selectedNeurons); const activeDatasets = new Set(formValues.selectedDatasets.map((dataset) => dataset.id)); createWorkspace(newWorkspaceId, formValues.workspaceName, activeDatasets, activeNeurons); @@ -138,12 +147,11 @@ const CreateNewWorkspaceDialog = ({ onCloseCreateWorkspace, showCreateWorkspaceD Neurons option.name} + options={neuronNames} renderOption={(props, option) => (
  • - {option.name} + {option}
  • )} onInputChange={onSearchNeurons} diff --git a/applications/visualizer/frontend/src/components/CustomAutocomplete.tsx b/applications/visualizer/frontend/src/components/CustomAutocomplete.tsx index bfda7929..2b52570c 100644 --- a/applications/visualizer/frontend/src/components/CustomAutocomplete.tsx +++ b/applications/visualizer/frontend/src/components/CustomAutocomplete.tsx @@ -3,9 +3,10 @@ import type { ChipProps } from "@mui/material/Chip"; import TextField from "@mui/material/TextField"; import type { SxProps } from "@mui/system"; import type React from "react"; + interface CustomAutocompleteProps { options: T[]; - getOptionLabel: (option: T) => string; + getOptionLabel?: (option: T) => string; renderOption: (props: React.HTMLAttributes, option: T) => React.ReactNode; renderInput?: (params: AutocompleteRenderInputParams) => React.ReactNode; groupBy?: (option: T) => string; @@ -19,10 +20,10 @@ interface CustomAutocompleteProps { ChipProps?: ChipProps; sx?: SxProps; componentsProps?: AutocompleteProps["componentsProps"]; - value?: any; - onChange: (v) => void; + value?: T[]; + onChange: (v: T | T[]) => void; disabled?: boolean; - onInputChange?: (v) => void; + onInputChange?: (v: string) => void; } const CommonAutocomplete = ({ @@ -65,7 +66,7 @@ const CommonAutocomplete = ({ groupBy={groupBy} renderGroup={renderGroup} renderOption={renderOption} - renderInput={(params) => onInputChange(e.target.value)} />} + renderInput={(params) => onInputChange?.(e.target.value)} />} sx={sx} componentsProps={componentsProps} /> diff --git a/applications/visualizer/frontend/src/components/ErrorAlert.tsx b/applications/visualizer/frontend/src/components/ErrorAlert.tsx new file mode 100644 index 00000000..a31253e4 --- /dev/null +++ b/applications/visualizer/frontend/src/components/ErrorAlert.tsx @@ -0,0 +1,70 @@ +import { Alert, AlertTitle, Box, Collapse } from "@mui/material"; +import { useEffect } from "react"; + +const ErrorAlert = ({ open, setOpen, errorMessage, autoCloseDuration = 5000 }) => { + useEffect(() => { + if (open) { + const timer = setTimeout(() => { + setOpen(false); + }, autoCloseDuration); + + return () => clearTimeout(timer); + } + }, [open, setOpen, autoCloseDuration]); + + return ( + + + setOpen(false)} + sx={{ + height: "auto !important", + padding: ".5rem 1rem !important", + "& .MuiSvgIcon-root": { + color: "#d32f2f", + "&:hover": { + backgroundColor: "transparent", + }, + }, + "& .MuiAlert-action": { + "& .MuiButtonBase-root": { + "& .MuiSvgIcon-root": { + color: "#d32f2f", + }, + "&:hover": { + backgroundColor: "transparent", + }, + }, + }, + }} + > + + An error has occurred + + {errorMessage} + + + + ); +}; + +export default ErrorAlert; diff --git a/applications/visualizer/frontend/src/components/ErrorBoundary.tsx b/applications/visualizer/frontend/src/components/ErrorBoundary.tsx new file mode 100644 index 00000000..a9996a0b --- /dev/null +++ b/applications/visualizer/frontend/src/components/ErrorBoundary.tsx @@ -0,0 +1,45 @@ +import { Component, type ReactNode } from "react"; + +interface ErrorBoundaryProps { + onError: (error: Error) => void; + children: ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; + errorMessage: string; +} + +class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { + hasError: false, + errorMessage: "", + }; + } + + static getDerivedStateFromError(error: Error) { + return { hasError: true, errorMessage: error.message }; + } + + componentDidCatch(error: Error) { + const { onError } = this.props; + this.setState({ hasError: true, errorMessage: error.message }); + onError(error); + } + + render() { + const { hasError } = this.state; + const { children } = this.props; + + return ( + <> + {children} + {hasError && null} + + ); + } +} + +export default ErrorBoundary; diff --git a/applications/visualizer/frontend/src/components/ViewerContainer/CustomEntitiesDropdown.tsx b/applications/visualizer/frontend/src/components/ViewerContainer/CustomEntitiesDropdown.tsx index 051327e8..d9a38e4d 100644 --- a/applications/visualizer/frontend/src/components/ViewerContainer/CustomEntitiesDropdown.tsx +++ b/applications/visualizer/frontend/src/components/ViewerContainer/CustomEntitiesDropdown.tsx @@ -5,7 +5,7 @@ import type React from "react"; import { useEffect, useRef, useState } from "react"; import { CheckIcon } from "../../icons"; import { vars } from "../../theme/variables.ts"; -import type { EnhancedNeuron } from "../../models/models.ts"; +import type { Neuron } from "../../rest/index.ts"; const { gray50, brand600 } = vars; @@ -25,8 +25,8 @@ interface CustomEntitiesDropdownProps { activeNeurons: Set; onNeuronClick?: (neuron: Option) => void; onSearchNeurons?: (value: string) => void; - setNeurons?: (neurons: Record) => void; - availableNeurons: Record; + setNeurons?: (neurons: Record) => void; + availableNeurons: Record; } const CustomEntitiesDropdown = ({ options, activeNeurons, onNeuronClick, onSearchNeurons, setNeurons, availableNeurons }: CustomEntitiesDropdownProps) => { @@ -86,7 +86,7 @@ const CustomEntitiesDropdown = ({ options, activeNeurons, onNeuronClick, onSearc InputProps={{ startAdornment: ( - + ), endAdornment: open && ( @@ -178,7 +178,7 @@ const CustomEntitiesDropdown = ({ options, activeNeurons, onNeuronClick, onSearc >
    - {option?.label?.length > 100 ? option?.label.slice(0, 100) + "..." : option?.label} + {option?.label?.length > 100 ? `${option?.label.slice(0, 100)}...` : option?.label} ))} diff --git a/applications/visualizer/frontend/src/components/ViewerContainer/CustomListItem.tsx b/applications/visualizer/frontend/src/components/ViewerContainer/CustomListItem.tsx index 9da39502..aa751522 100644 --- a/applications/visualizer/frontend/src/components/ViewerContainer/CustomListItem.tsx +++ b/applications/visualizer/frontend/src/components/ViewerContainer/CustomListItem.tsx @@ -77,7 +77,7 @@ const CustomListItem = ({ <> + } @@ -116,7 +116,7 @@ const CustomListItem = ({ /> )} - {data?.label?.length > 32 ? data?.label.slice(0, 32) + "..." : data?.label} + {data?.label?.length > 32 ? `${data?.label.slice(0, 32)}...` : data?.label} {showTooltip && ( diff --git a/applications/visualizer/frontend/src/components/ViewerContainer/CustomSwitch.tsx b/applications/visualizer/frontend/src/components/ViewerContainer/CustomSwitch.tsx index 8fc8fda7..4c2b9048 100644 --- a/applications/visualizer/frontend/src/components/ViewerContainer/CustomSwitch.tsx +++ b/applications/visualizer/frontend/src/components/ViewerContainer/CustomSwitch.tsx @@ -1,6 +1,7 @@ import Switch from "@mui/material/Switch"; import Tooltip from "@mui/material/Tooltip"; import type React from "react"; +import { forwardRef } from "react"; import { vars } from "../../theme/variables.ts"; const { white, brand600, gray100 } = vars; @@ -16,57 +17,60 @@ interface CustomSwitchProps { disabled?: boolean; } -const CustomSwitch: React.FC = ({ width, height, thumbDimension, checkedPosition, checked, onChange, showTooltip, disabled }) => { - return ( - - ({ - marginRight: ".5rem", - width: width ?? 23, - height: height ?? 13, - padding: 0, - "& .MuiSwitch-switchBase": { +const CustomSwitch = forwardRef( + ({ width, height, thumbDimension, checkedPosition, checked, onChange, showTooltip, disabled }, ref) => { + return ( + + ({ + marginRight: ".5rem", + width: width ?? 23, + height: height ?? 13, padding: 0, - margin: "0.0938rem", - transitionDuration: "300ms", - "&.Mui-checked": { - transform: checkedPosition ?? "translateX(0.5775rem)", - color: white, - "& + .MuiSwitch-track": { - backgroundColor: brand600, - opacity: 1, - border: 0, + "& .MuiSwitch-switchBase": { + padding: 0, + margin: "0.0938rem", + transitionDuration: "300ms", + "&.Mui-checked": { + transform: checkedPosition ?? "translateX(0.5775rem)", + color: white, + "& + .MuiSwitch-track": { + backgroundColor: brand600, + opacity: 1, + border: 0, + }, + "&.Mui-disabled + .MuiSwitch-track": { + opacity: 0.5, + }, }, - "&.Mui-disabled + .MuiSwitch-track": { - opacity: 0.5, + "&.Mui-disabled .MuiSwitch-thumb": { + color: gray100, }, }, - "&.Mui-disabled .MuiSwitch-thumb": { - color: gray100, + "& .MuiSwitch-thumb": { + boxSizing: "border-box", + width: thumbDimension ?? 10.24, + height: thumbDimension ?? 10.24, + boxShadow: "none", }, - }, - "& .MuiSwitch-thumb": { - boxSizing: "border-box", - width: thumbDimension ?? 10.24, - height: thumbDimension ?? 10.24, - boxShadow: "none", - }, - "& .MuiSwitch-track": { - borderRadius: 26 / 2, - backgroundColor: gray100, - opacity: 1, - transition: theme.transitions.create(["background-color"], { - duration: 500, - }), - }, - })} - /> - - ); -}; + "& .MuiSwitch-track": { + borderRadius: 26 / 2, + backgroundColor: gray100, + opacity: 1, + transition: theme.transitions.create(["background-color"], { + duration: 500, + }), + }, + })} + /> + + ); + }, +); export default CustomSwitch; diff --git a/applications/visualizer/frontend/src/components/ViewerContainer/DataSets.tsx b/applications/visualizer/frontend/src/components/ViewerContainer/DataSets.tsx index 0bd2a3dc..dde2cc1d 100644 --- a/applications/visualizer/frontend/src/components/ViewerContainer/DataSets.tsx +++ b/applications/visualizer/frontend/src/components/ViewerContainer/DataSets.tsx @@ -18,20 +18,23 @@ const categorizeDatasets = (datasets: Dataset[]) => { L1: [], L2: [], L3: [], + L4: [], Adult: [], }; - datasets.forEach((dataset) => { - if (dataset.visualTime >= 0 && dataset.visualTime < 10) { - categories["L1"].push(dataset); - } else if (dataset.visualTime >= 10 && dataset.visualTime < 20) { - categories["L2"].push(dataset); - } else if (dataset.visualTime >= 20 && dataset.visualTime < 30) { - categories["L3"].push(dataset); - } else if (dataset.visualTime >= 30) { - categories["Adult"].push(dataset); + for (const dataset of datasets) { + if (dataset.visualTime >= 0 && dataset.visualTime < 16) { + categories.L1.push(dataset); + } else if (dataset.visualTime >= 16 && dataset.visualTime < 25) { + categories.L2.push(dataset); + } else if (dataset.visualTime >= 25 && dataset.visualTime < 34) { + categories.L3.push(dataset); + } else if (dataset.visualTime >= 34 && dataset.visualTime < 45) { + categories.L4.push(dataset); + } else if (dataset.visualTime >= 45) { + categories.Adult.push(dataset); } - }); + } return categories; }; @@ -97,6 +100,7 @@ const DataSets = ({ children }) => { L1: [], L2: [], L3: [], + L4: [], Adult: [], }; @@ -106,10 +110,18 @@ const DataSets = ({ children }) => { const filteredActiveList = inputValue ? activeDatasetsList.filter((dataset) => dataset.name.toLowerCase().includes(inputValue)) : activeDatasetsList; - setFilteredDatasets(filteredCategories); - setFilterActiveDatasets(filteredActiveList); + if (filterGroupsValue === "All") { + setFilteredDatasets(filteredCategories); + setFilterActiveDatasets(filteredActiveList); + } else { + const activeGroup = filteredCategories[filterGroupsValue]; + const updatedFilteredDatasets = { ...filteredActiveList, [filterGroupsValue]: activeGroup }; + // @ts-ignore + setFilteredDatasets(updatedFilteredDatasets); + const updatedFilteredActiveDatasets = filteredActiveList.filter((dataset) => activeGroup.some((catDataset) => catDataset.id === dataset.id)); + setFilterActiveDatasets(updatedFilteredActiveDatasets); + } }; - const onSelectGroupChange = (e) => { const selectedGroup = e.target.value; setFilterGroupsValue(selectedGroup); @@ -133,11 +145,11 @@ const DataSets = ({ children }) => { const getDatasetsTypes = (datasets: { [key: string]: Dataset }) => { const types = new Set(); - Object.values(datasets).forEach((dataset) => { + for (const dataset of Object.values(datasets)) { if (dataset.type) { types.add(dataset.type); } - }); + } return Array.from(types); }; @@ -149,6 +161,7 @@ const DataSets = ({ children }) => { L1: [], L2: [], L3: [], + L4: [], Adult: [], }; @@ -213,7 +226,7 @@ const DataSets = ({ children }) => { InputProps={{ startAdornment: ( - + ), }} @@ -306,8 +319,8 @@ const DataSets = ({ children }) => { }, }} > - {datasetsTypes.map((type, i) => ( - handleTypeSelect(type)}> + {datasetsTypes.map((type) => ( + handleTypeSelect(type)}> (null); - const { workspaces, setSelectedWorkspacesIds, setViewMode, selectedWorkspacesIds, viewMode, setCurrentWorkspace } = useGlobalContext(); + const { workspaces, setSelectedWorkspacesIds, setViewMode, selectedWorkspacesIds, viewMode, setCurrentWorkspace, serializeGlobalContext } = + useGlobalContext(); const open = Boolean(anchorEl); const handleClick = (event: React.MouseEvent) => { @@ -133,7 +134,7 @@ const Header = ({ } else { setViewMode(ViewMode.Compare); } - }, [selectedWorkspacesIds]); + }, [selectedWorkspacesIds, setViewMode]); return ( <> {VIEW_OPTIONS.map((item, index) => { return ( - + @@ -186,7 +187,17 @@ const Header = ({ {viewMode === ViewMode.Default && ( - )} diff --git a/applications/visualizer/frontend/src/components/ViewerContainer/Neurons.tsx b/applications/visualizer/frontend/src/components/ViewerContainer/Neurons.tsx index ba766c77..ecbb345a 100644 --- a/applications/visualizer/frontend/src/components/ViewerContainer/Neurons.tsx +++ b/applications/visualizer/frontend/src/components/ViewerContainer/Neurons.tsx @@ -1,21 +1,19 @@ import AddIcon from "@mui/icons-material/Add"; import { Box, IconButton, Stack, Typography } from "@mui/material"; import Tooltip from "@mui/material/Tooltip"; -import { debounce } from "lodash"; -import { useCallback, useState } from "react"; +import { useState } from "react"; import { useGlobalContext } from "../../contexts/GlobalContext.tsx"; import type { Neuron } from "../../rest"; -import { NeuronsService } from "../../rest"; import { vars } from "../../theme/variables.ts"; import CustomEntitiesDropdown from "./CustomEntitiesDropdown.tsx"; import CustomListItem from "./CustomListItem.tsx"; -import type { EnhancedNeuron } from "../../models/models.ts"; +import { Visibility, type ViewerData } from "../../models/models.ts"; const { gray900, gray500 } = vars; -const mapNeuronsToListItem = (neuron: string, isActive: boolean) => ({ +const mapNeuronsToListItem = (neuron: string, visibility: ViewerData) => ({ id: neuron, label: neuron, - checked: isActive, + checked: Object.values(visibility).every((e) => e === undefined || e.visibility === Visibility.Visible), }); const mapNeuronsAvailableNeuronsToOptions = (neuron: Neuron) => ({ id: neuron.name, @@ -24,62 +22,46 @@ const mapNeuronsAvailableNeuronsToOptions = (neuron: Neuron) => ({ }); const Neurons = ({ children }) => { - const { workspaces, datasets, currentWorkspaceId } = useGlobalContext(); - const currentWorkspace = workspaces[currentWorkspaceId]; + const { getCurrentWorkspace } = useGlobalContext(); + const currentWorkspace = getCurrentWorkspace(); + const activeNeurons = currentWorkspace.activeNeurons; - const recentNeurons = Object.values(currentWorkspace.availableNeurons).filter((neuron) => neuron.isInteractant); const availableNeurons = currentWorkspace.availableNeurons; + const groups = currentWorkspace.neuronGroups; const [neurons, setNeurons] = useState(availableNeurons); - const handleSwitchChange = async (neuronId: string, checked: boolean) => { - const neuron = availableNeurons[neuronId]; - - if (!neuron) return; - if (checked) { - await currentWorkspace.activateNeuron(neuron); + const handleSwitchChange = async (neuronId: string, isChecked: boolean) => { + if (isChecked) { + await currentWorkspace.showNeuron(neuronId); } else { - await currentWorkspace.deactivateNeuron(neuronId); + await currentWorkspace.hideNeuron(neuronId); } }; const onNeuronClick = (option) => { const neuron = availableNeurons[option.id]; + if (neuron && !activeNeurons.has(option.id)) { - currentWorkspace.activateNeuron(neuron); + currentWorkspace.activateNeuron(neuron).showNeuron(neuron.name); } else { - currentWorkspace.deleteNeuron(option.id); + currentWorkspace.deactivateNeuron(option.id); } }; const handleDeleteNeuron = (neuronId: string) => { - currentWorkspace.deleteNeuron(neuronId); - }; - - const fetchNeurons = async (name: string, datasetsIds: { id: string }[]) => { - try { - const ids = datasetsIds.map((dataset) => dataset.id); - const response = await NeuronsService.searchCells({ name: name, datasetIds: ids }); - - // Convert the object to a Record - const neuronsRecord = Object.entries(response).reduce((acc: Record, [_, neuron]: [string, EnhancedNeuron]) => { - acc[neuron.name] = neuron; - return acc; - }, {}); - - setNeurons(neuronsRecord); - } catch (error) { - console.error("Failed to fetch datasets", error); - } + currentWorkspace.deactivateNeuron(neuronId); }; - const debouncedFetchNeurons = useCallback(debounce(fetchNeurons, 300), []); - - const onSearchNeurons = (value) => { - const datasetsIds = Object.keys(datasets); - debouncedFetchNeurons(value, datasetsIds); + const onSearchNeurons = (nameFragment) => { + const filteredNeurons = Object.fromEntries( + Object.entries(availableNeurons).filter(([_, neuron]) => neuron.name.toLowerCase().startsWith(nameFragment.toLowerCase())), + ); + setNeurons(filteredNeurons); }; - const autoCompleteOptions = Object.values(neurons).map((neuron: Neuron) => mapNeuronsAvailableNeuronsToOptions(neuron)); + const autoCompleteOptions = Object.values(neurons) + .map((neuron: Neuron) => mapNeuronsAvailableNeuronsToOptions(neuron)) + .sort((a, b) => a.label.localeCompare(b.label)); return ( { - {Array.from(recentNeurons).map((neuron) => ( + {Array.from(activeNeurons).map((neuronId) => ( { deleteTooltipTitle="Remove neuron from the workspace" /> ))} + + + All Groups + + + + + + + + {Array.from(Object.keys(groups)).map((groupId) => ( + console.log("switch")} + onDelete={() => console.log("delete")} + deleteTooltipTitle="Remove group from the workspace" + /> + ))} diff --git a/applications/visualizer/frontend/src/components/ViewerContainer/WorkspaceSelector.tsx b/applications/visualizer/frontend/src/components/ViewerContainer/WorkspaceSelector.tsx index 8407d309..ec782710 100644 --- a/applications/visualizer/frontend/src/components/ViewerContainer/WorkspaceSelector.tsx +++ b/applications/visualizer/frontend/src/components/ViewerContainer/WorkspaceSelector.tsx @@ -2,6 +2,7 @@ import { Box, Button, Menu, MenuItem, Typography } from "@mui/material"; import type React from "react"; import { useGlobalContext } from "../../contexts/GlobalContext.tsx"; import { CheckIcon, DownIcon } from "../../icons"; +import type { Workspace } from "../../models/workspace.ts"; import { vars } from "../../theme/variables.ts"; const { gray500, gray50, brand600 } = vars; @@ -11,7 +12,7 @@ interface WorkspaceSelectorProps { openWorkspace: boolean; handleClickWorkspace: (event: React.MouseEvent) => void; handleCloseWorkspace: () => void; - onClickWorkspace: (workspace: any) => void; + onClickWorkspace: (workspace: Workspace) => void; } const WorkspaceSelector: React.FC = ({ diff --git a/applications/visualizer/frontend/src/components/ViewerSettings.tsx b/applications/visualizer/frontend/src/components/ViewerSettings.tsx index 40890b18..1f7f25e8 100644 --- a/applications/visualizer/frontend/src/components/ViewerSettings.tsx +++ b/applications/visualizer/frontend/src/components/ViewerSettings.tsx @@ -5,7 +5,7 @@ import { useDispatch } from "react-redux"; import { useGlobalContext } from "../contexts/GlobalContext.tsx"; import { CloseIcon, LinkIcon } from "../icons"; import { emDataViewerWidget, threeDViewerWidget, twoDViewerWidget } from "../layout-manager/widgets.ts"; -import { ViewerType } from "../models/models.ts"; +import { ViewerSynchronizationPair, ViewerType } from "../models/models.ts"; import { vars } from "../theme/variables.ts"; import CustomSwitch from "./ViewerContainer/CustomSwitch.tsx"; @@ -27,14 +27,17 @@ const SyncViewersData = [ { primaryText: "Connectivity graph", secondaryText: "Instance details", + syncPair: ViewerSynchronizationPair.Graph_InstanceDetails, }, { primaryText: "3D viewer", secondaryText: "EM viewer", + syncPair: ViewerSynchronizationPair.ThreeD_EM, }, { primaryText: "Connectivity graph", secondaryText: "3D viewer", + syncPair: ViewerSynchronizationPair.Graph_ThreeD, }, ]; @@ -65,8 +68,8 @@ const ViewerSettings = ({ open, toggleDrawer }) => { return; } }; - const handleChangeSynchronizations = (_, index, status) => { - currentWorkspace.updateViewerSynchronizationStatus(index, !status); + const handleChangeSynchronizations = (_, syncPair) => { + currentWorkspace.switchViewerSynchronizationStatus(syncPair); }; return ( @@ -164,13 +167,13 @@ const ViewerSettings = ({ open, toggleDrawer }) => { Sync viewers - {SyncViewersData?.map((data, index) => ( - + {SyncViewersData?.map((data) => ( + {data.primaryText} handleChangeSynchronizations(e, index, currentWorkspace?.synchronizations[index])} + onClick={(e) => handleChangeSynchronizations(e, data.syncPair)} > diff --git a/applications/visualizer/frontend/src/components/WorkspaceComponent.tsx b/applications/visualizer/frontend/src/components/WorkspaceComponent.tsx index 12105cf1..141adf49 100644 --- a/applications/visualizer/frontend/src/components/WorkspaceComponent.tsx +++ b/applications/visualizer/frontend/src/components/WorkspaceComponent.tsx @@ -119,7 +119,7 @@ function WorkspaceComponent({ sidebarOpen }) { const workspaceKeys = Object.keys(workspaces); const workspaceIndex = workspaceKeys.indexOf(workspaceId); - let workspaceIdToView; + let workspaceIdToView: string; if (workspaceKeys.length === 2) { workspaceIdToView = workspaceKeys.find((id) => id !== workspaceId); diff --git a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx index cf5700b2..23ba2c21 100644 --- a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx @@ -1,6 +1,6 @@ import { Box } from "@mui/material"; import "ol/ol.css"; -import { type Feature, Map, View } from "ol"; +import { type Feature, Map as OLMap, View } from "ol"; import type { FeatureLike } from "ol/Feature"; import ScaleLine from "ol/control/ScaleLine"; import { shiftKeyOnly } from "ol/events/condition"; @@ -98,6 +98,7 @@ const newEMLayer = (dataset: Dataset, slice: number): TileLayer => { // url: `emdata/${slice}/{x}_{y}_{z}.jpg`, url: getEMDataURL(dataset, slice), projection: projection, + crossOrigin: "anonymous", }), zIndex: 0, }); @@ -126,10 +127,13 @@ const EMStackViewer = () => { const startSlice = 537; const ringSize = 11; - const mapRef = useRef(null); + const mapRef = useRef(null); const currSegLayer = useRef | null>(null); const clickedFeature = useRef(null); + let ringEM: SlidingRing>; + let ringSeg: SlidingRing>; + // const debugLayer = new TileLayer({ // source: new TileDebug({ // projection: projection, @@ -154,7 +158,7 @@ const EMStackViewer = () => { return; } - const map = new Map({ + const map = new OLMap({ target: "emviewer", layers: [], view: new View({ @@ -171,7 +175,7 @@ const EMStackViewer = () => { const minZoomAvailable = tilegrid.getMinZoom(); map.getView().setZoom(minZoomAvailable); - const ringEM = new SlidingRing({ + ringEM = new SlidingRing({ cacheSize: ringSize, startAt: startSlice, extent: [minSlice, maxSlice], @@ -192,7 +196,7 @@ const EMStackViewer = () => { }, }); - const ringSeg = new SlidingRing({ + ringSeg = new SlidingRing({ cacheSize: ringSize, startAt: startSlice, extent: [minSlice, maxSlice], @@ -269,6 +273,10 @@ const EMStackViewer = () => { }; const onResetView = () => { + // reset sliding window + ringEM.goto(startSlice); + ringSeg.goto(startSlice); + if (!mapRef.current) return; const view = mapRef.current.getView(); @@ -297,7 +305,7 @@ const EMStackViewer = () => { export default EMStackViewer; -function printEMView(map: Map) { +function printEMView(map: OLMap) { const mapCanvas = document.createElement("canvas"); const size = map.getSize(); @@ -310,7 +318,7 @@ function printEMView(map: Map) { if (canvas.width > 0) { const opacity = canvas.parentNode.style.opacity || canvas.style.opacity; mapContext.globalAlpha = opacity === "" ? 1 : Number(opacity); - let matrix; + let matrix: Array; const transform = canvas.style.transform; if (transform) { // Get the transform parameters from the style's transform matrix diff --git a/applications/visualizer/frontend/src/components/viewers/ThreeD/STLMesh.tsx b/applications/visualizer/frontend/src/components/viewers/ThreeD/STLMesh.tsx index 773d407b..bd5a36c8 100644 --- a/applications/visualizer/frontend/src/components/viewers/ThreeD/STLMesh.tsx +++ b/applications/visualizer/frontend/src/components/viewers/ThreeD/STLMesh.tsx @@ -1,12 +1,13 @@ -import type { FC } from "react"; import { Outlines } from "@react-three/drei"; -import { useGlobalContext } from "../../../contexts/GlobalContext"; +import type { ThreeEvent } from "@react-three/fiber"; +import type { FC } from "react"; import { useSelector } from "react-redux"; -import type { Workspace } from "../../../models/workspace"; -import type { RootState } from "../../../layout-manager/layoutManagerFactory"; import { type BufferGeometry, DoubleSide, NormalBlending } from "three"; -import type { ThreeEvent } from "@react-three/fiber"; +import { useGlobalContext } from "../../../contexts/GlobalContext"; import { getFurthestIntersectedObject } from "../../../helpers/threeDHelpers"; +import type { RootState } from "../../../layout-manager/layoutManagerFactory"; +import type { Workspace } from "../../../models"; +import { ViewerType } from "../../../models"; import { OUTLINE_COLOR, OUTLINE_THICKNESS } from "../../../settings/threeDSettings"; interface Props { @@ -22,14 +23,21 @@ const STLMesh: FC = ({ id, color, opacity, renderOrder, isWireframe, stl const { workspaces } = useGlobalContext(); const workspaceId = useSelector((state: RootState) => state.workspaceId); const workspace: Workspace = workspaces[workspaceId]; + const selectedNeurons = workspace.getViewerSelecedNeurons(ViewerType.Graph); + const isSelected = selectedNeurons.includes(id); + const onClick = (event: ThreeEvent) => { const clicked = getFurthestIntersectedObject(event); + const { id } = clicked.userData; if (clicked) { - workspace.toggleSelectedNeuron(clicked.userData.id); + if (isSelected) { + console.log(`Neurons selected: ${id}`); + } else { + console.log(`Neurons un selected: ${id}`); + } } }; - const isSelected = id in workspace.selectedNeurons; return ( diff --git a/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx b/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx index 90004df9..2ef4dd95 100644 --- a/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx @@ -18,13 +18,13 @@ import { useSelector } from "react-redux"; import { useGlobalContext } from "../../../contexts/GlobalContext.tsx"; import { CheckIcon, CloseIcon } from "../../../icons"; import type { RootState } from "../../../layout-manager/layoutManagerFactory.ts"; +import type { Dataset } from "../../../rest"; import { vars } from "../../../theme/variables.ts"; import CustomAutocomplete from "../../CustomAutocomplete.tsx"; import Gizmo from "./Gizmo.tsx"; import Loader from "./Loader.tsx"; import STLViewer from "./STLViewer.tsx"; import SceneControls from "./SceneControls.tsx"; -import type { Dataset } from "../../../rest"; const { gray100, gray600 } = vars; export interface Instance { @@ -55,7 +55,7 @@ function ThreeDViewer() { id: "nerve_ring", url: "resources/nervering-SEM_adult.stl", color: "white", - opacity: 0.5, + opacity: 0.3, }, { id: "adal_sem", diff --git a/applications/visualizer/frontend/src/components/viewers/TwoD/ContextMenu.tsx b/applications/visualizer/frontend/src/components/viewers/TwoD/ContextMenu.tsx index 5d1177d8..cfca9464 100644 --- a/applications/visualizer/frontend/src/components/viewers/TwoD/ContextMenu.tsx +++ b/applications/visualizer/frontend/src/components/viewers/TwoD/ContextMenu.tsx @@ -1,270 +1,418 @@ +import { + ArrowRightOutlined, + CallSplitOutlined, + CloseFullscreen, + FormatAlignJustifyOutlined, + GroupOutlined, + HubOutlined, + MergeOutlined, + OpenInFull, + VisibilityOutlined, + WorkspacesOutlined, +} from "@mui/icons-material"; +import { Box, Divider, Menu, MenuItem, Popover } from "@mui/material"; +import type { Core } from "cytoscape"; import type React from "react"; -import { useMemo } from "react"; -import { Menu, MenuItem } from "@mui/material"; -import { type NeuronGroup, ViewerType } from "../../../models"; -import { calculateMeanPosition, calculateSplitPositions, isNeuronClass } from "../../../helpers/twoD/twoDHelpers.ts"; +import { useEffect, useMemo, useState } from "react"; +import { alignNeurons, distributeNeurons } from "../../../helpers/twoD/alignHelper.ts"; +import { groupNeurons, removeNodeFromGroup } from "../../../helpers/twoD/groupHelper.ts"; +import { processNeuronJoin, processNeuronSplit } from "../../../helpers/twoD/splitJoinHelper.ts"; import { useSelectedWorkspace } from "../../../hooks/useSelectedWorkspace.ts"; -import type { Position } from "cytoscape"; -import type { GraphViewerData } from "../../../models/models.ts"; +import { AlignBottomIcon, AlignLeftIcon, AlignRightIcon, AlignTopIcon, DistributeHorizontallyIcon, DistributeVerticallyIcon } from "../../../icons"; +import { Alignment, ViewerType, Visibility } from "../../../models"; +import { emptyViewerData } from "../../../models/models.ts"; +import { vars } from "../../../theme/variables.ts"; + +const { gray700 } = vars; interface ContextMenuProps { open: boolean; onClose: () => void; position: { mouseX: number; mouseY: number } | null; setSplitJoinState: React.Dispatch; join: Set }>>; - setHiddenNodes: React.Dispatch>>; + openGroups: Set; + setOpenGroups: React.Dispatch>>; + cy: Core; } -const ContextMenu: React.FC = ({ open, onClose, position, setSplitJoinState, setHiddenNodes }) => { +const ContextMenu: React.FC = ({ open, onClose, position, setSplitJoinState, openGroups, setOpenGroups, cy }) => { const workspace = useSelectedWorkspace(); + const [submenuAnchorEl, setSubmenuAnchorEl] = useState(null); + const selectedNeurons = workspace.getViewerSelecedNeurons(ViewerType.Graph); + + const submenuOpen = Boolean(submenuAnchorEl); + + const handlePopoverOpen = (event: React.MouseEvent) => { + setSubmenuAnchorEl(event.currentTarget); + }; + const handlePopoverClose = () => { + setSubmenuAnchorEl(null); + }; + + const handleContextMenuClose = () => { + onClose(); + handlePopoverClose(); // Ensure the submenu is also closed when the context menu closes. + }; + + const handleAlignOption = (option: Alignment) => { + alignNeurons(option, selectedNeurons, cy); + setSubmenuAnchorEl(null); + onClose(); + setSubmenuAnchorEl(null); + }; + + const handleDistributeOption = (option: Alignment) => { + distributeNeurons(option, selectedNeurons, cy); + setSubmenuAnchorEl(null); + onClose(); + }; const handleHide = () => { - setHiddenNodes((prevHiddenNodes) => { - const newHiddenNodes = new Set([...prevHiddenNodes]); - workspace.selectedNeurons.forEach((neuronId) => { - newHiddenNodes.add(neuronId); - }); - return newHiddenNodes; + workspace.customUpdate((draft) => { + for (const neuronId of selectedNeurons) { + if (!(neuronId in draft.visibilities)) { + draft.visibilities[neuronId] = emptyViewerData(Visibility.Hidden); + } else { + draft.visibilities[neuronId][ViewerType.Graph].visibility = Visibility.Hidden; + } + } + draft.clearSelection(ViewerType.Graph); }); - workspace.clearSelectedNeurons(); onClose(); }; const handleGroup = () => { - const newGroupId = `group_${Date.now()}`; - const newGroupNeurons = new Set(); - const groupsToDelete = new Set(); - - for (const neuronId of workspace.selectedNeurons) { - const group = workspace.neuronGroups[neuronId]; - if (group) { - for (const groupedNeuronId of group.neurons) { - newGroupNeurons.add(groupedNeuronId); + const selectedNeuronsSet = new Set(selectedNeurons); + const { newGroupId, newGroup, groupsToDelete } = groupNeurons(selectedNeuronsSet, workspace); + + workspace.customUpdate((draft) => { + // Add the new group + draft.neuronGroups[newGroupId] = newGroup; + draft.visibilities[newGroupId] = emptyViewerData(Visibility.Visible); + + // Remove the old groups that were merged into the new group + for (const groupId of groupsToDelete) { + delete draft.neuronGroups[groupId]; + delete draft.visibilities[groupId]; + } + + // Clear the current selection and select the new group + draft.setSelection([newGroupId], ViewerType.Graph); + }); + + setOpenGroups((prevOpenGroups: Set) => { + const updatedOpenGroups = new Set(prevOpenGroups); + let wasGroupOpen = false; + + for (const groupId of groupsToDelete) { + if (updatedOpenGroups.has(groupId)) { + updatedOpenGroups.delete(groupId); + wasGroupOpen = true; } - groupsToDelete.add(neuronId); - } else { - newGroupNeurons.add(neuronId); } - } - const newGroup: NeuronGroup = { - id: newGroupId, - name: newGroupId, - color: "#9FEE9A", - neurons: newGroupNeurons, - }; + // Only add the new group if any of the deleted groups were open + if (wasGroupOpen) { + updatedOpenGroups.add(newGroupId); + } - workspace.customUpdate((draft) => { - draft.neuronGroups[newGroupId] = newGroup; - groupsToDelete.forEach((groupId) => delete draft.neuronGroups[groupId]); - draft.selectedNeurons.clear(); - draft.selectedNeurons.add(newGroupId); + return updatedOpenGroups; }); + onClose(); }; - const handleUngroup = () => { + const groupsToRemoveFromOpen = new Set(); + workspace.customUpdate((draft) => { const nextSelected = new Set(); - for (const elementId of draft.selectedNeurons) { + + for (const elementId of selectedNeurons) { if (draft.neuronGroups[elementId]) { + // Handle the case where the selected element is a group const group = draft.neuronGroups[elementId]; for (const groupedNeuronId of group.neurons) { nextSelected.add(groupedNeuronId); + removeNodeFromGroup(cy, groupedNeuronId, true); + } + delete draft.neuronGroups[elementId]; // Delete the entire group + if (openGroups.has(elementId)) { + groupsToRemoveFromOpen.add(elementId); + } + } else { + // Handle the case where the selected element is a neuron within a group + for (const [groupId, group] of Object.entries(draft.neuronGroups)) { + if (group.neurons.has(elementId)) { + group.neurons.delete(elementId); // Remove the neuron from the group + nextSelected.add(elementId); + removeNodeFromGroup(cy, elementId, true); + + if (group.neurons.size === 0) { + // If the group is now empty, delete it + delete draft.neuronGroups[groupId]; + if (openGroups.has(groupId)) { + groupsToRemoveFromOpen.add(groupId); + } + } + } } - delete draft.neuronGroups[elementId]; } } - draft.selectedNeurons = nextSelected; + + draft.setSelection(Array.from(nextSelected), ViewerType.Graph); + }); + + // Remove groups from the openGroups set + setOpenGroups((prevOpenGroups: Set) => { + const updatedOpenGroups = new Set(prevOpenGroups); + for (const groupId of groupsToRemoveFromOpen) { + updatedOpenGroups.delete(groupId); + } + return updatedOpenGroups; }); + onClose(); }; const handleSplit = () => { setSplitJoinState((prevState) => { - const newSplit = new Set(prevState.split); - const newJoin = new Set(prevState.join); - - const newSelectedNeurons = new Set(workspace.selectedNeurons); - const graphViewDataUpdates: Record = {}; - - workspace.selectedNeurons.forEach((neuronId) => { - if (isNeuronClass(neuronId, workspace)) { - newSplit.add(neuronId); - newSelectedNeurons.delete(neuronId); - - const individualNeurons = Object.values(workspace.availableNeurons) - .filter((neuron) => { - return neuron.nclass === neuronId && neuron.nclass !== neuron.name; - }) - .map((neuron) => neuron.name); - - // Calculate the positions for the individual neurons - const basePosition = workspace.availableNeurons[neuronId].viewerData[ViewerType.Graph]?.defaultPosition || { - x: 0, - y: 0, - }; - const positions = calculateSplitPositions(individualNeurons, basePosition); - - // Update the selected neurons with individual neurons - individualNeurons.forEach((neuronName) => { - newSelectedNeurons.add(neuronName); - // Only set the position if it doesn't exist yet - if (!workspace.availableNeurons[neuronName].viewerData[ViewerType.Graph]?.defaultPosition) { - graphViewDataUpdates[neuronName] = { position: positions[neuronName], visibility: true }; - } else { - graphViewDataUpdates[neuronName] = { visibility: true }; - } - }); - - // Remove the corresponding class from the toJoin set - newJoin.forEach((joinNeuronId) => { - if (workspace.availableNeurons[joinNeuronId].nclass === neuronId) { - newJoin.delete(joinNeuronId); - } - }); - - graphViewDataUpdates[neuronId] = { visibility: false }; - } - }); - - // Update the selected neurons in the workspace - updateWorkspace(newSelectedNeurons, graphViewDataUpdates); - - return { split: newSplit, join: newJoin }; + return processNeuronSplit(workspace, prevState); }); onClose(); }; const handleJoin = () => { setSplitJoinState((prevState) => { - const newJoin = new Set(prevState.join); - const newSplit = new Set(prevState.split); - - const newSelectedNeurons = new Set(workspace.selectedNeurons); - const graphViewDataUpdates: Record = {}; - - workspace.selectedNeurons.forEach((neuronId) => { - const neuronClass = workspace.availableNeurons[neuronId].nclass; - - const individualNeurons = Object.values(workspace.availableNeurons).filter((neuron) => neuron.nclass === neuronClass && neuron.name !== neuronClass); - const individualNeuronIds = individualNeurons.map((neuron) => neuron.name); - - // Calculate and set the class position if not set already - const classPosition = calculateMeanPosition(individualNeuronIds, workspace); - - if (!workspace.availableNeurons[neuronClass].viewerData[ViewerType.Graph]?.defaultPosition) { - graphViewDataUpdates[neuronClass] = { position: classPosition, visibility: true }; - } else { - graphViewDataUpdates[neuronClass] = { ...graphViewDataUpdates[neuronClass], visibility: true }; - } - // Remove the individual neurons from the selected neurons and add the class neuron - individualNeuronIds.forEach((neuronName) => { - newSelectedNeurons.delete(neuronName); - newJoin.add(neuronName); - - // Set individual neurons' visibility to false - graphViewDataUpdates[neuronName] = { visibility: false }; - }); - newSelectedNeurons.add(neuronClass); - - // Remove the corresponding cells from the toSplit set - newSplit.forEach((splitNeuronId) => { - if (workspace.availableNeurons[splitNeuronId].nclass === neuronClass) { - newSplit.delete(splitNeuronId); - } - }); - }); - - // Update the selected neurons in the workspace - updateWorkspace(newSelectedNeurons, graphViewDataUpdates); - - return { split: newSplit, join: newJoin }; + return processNeuronJoin(workspace, prevState); }); onClose(); }; - const updateWorkspace = (newSelectedNeurons: Set, graphViewDataUpdates: Record>) => { - workspace.customUpdate((draft) => { - // Update the selected neurons - draft.selectedNeurons = newSelectedNeurons; - - // Update the positions and visibility for the individual neurons and class neuron - Object.entries(graphViewDataUpdates).forEach(([neuronName, update]) => { - if (draft.availableNeurons[neuronName]) { - if (update.defaultPosition !== undefined) { - draft.availableNeurons[neuronName].viewerData[ViewerType.Graph].defaultPosition = update.defaultPosition; - } - draft.availableNeurons[neuronName].viewerData[ViewerType.Graph].visibility = update.visibility; - } - }); - }); - }; - const handleAddToWorkspace = () => { workspace.customUpdate((draft) => { - workspace.selectedNeurons.forEach((neuronId) => { + for (const neuronId of selectedNeurons) { const group = workspace.neuronGroups[neuronId]; if (group) { - group.neurons.forEach((groupedNeuronId) => { + for (const groupedNeuronId of group.neurons) { draft.activeNeurons.add(groupedNeuronId); - }); + draft.visibilities[groupedNeuronId] = emptyViewerData(Visibility.Visible); + } } else { draft.activeNeurons.add(neuronId); + draft.visibilities[neuronId] = emptyViewerData(Visibility.Visible); } - }); + } }); onClose(); }; + const handleOpenGroup = () => { + for (const neuronId of selectedNeurons) { + if (workspace.neuronGroups[neuronId] && !openGroups.has(neuronId)) { + // Mark the group as open + setOpenGroups((prevOpenGroups: Set) => { + const updatedOpenGroups = new Set(prevOpenGroups); + updatedOpenGroups.add(neuronId); + return updatedOpenGroups; + }); + } + } + onClose(); + }; + const handleCloseGroup = () => { + for (const neuronId of selectedNeurons) { + if (workspace.neuronGroups[neuronId] && openGroups.has(neuronId)) { + // Mark the group as closed + setOpenGroups((prevOpenGroups: Set) => { + const updatedOpenGroups = new Set(prevOpenGroups); + updatedOpenGroups.delete(neuronId); + return updatedOpenGroups; + }); + } + } + onClose(); + }; + const groupEnabled = useMemo(() => { - return Array.from(workspace.selectedNeurons).some((neuronId) => !workspace.neuronGroups[neuronId]); - }, [workspace.selectedNeurons, workspace.neuronGroups]); + const groupOrPartOfGroupSet = new Set(); + let nonGroupOrPartCount = 0; + for (const neuronId of selectedNeurons) { + const isGroup = Boolean(workspace.neuronGroups[neuronId]); + const isPartOfGroup = Object.entries(workspace.neuronGroups).find(([, group]) => group.neurons.has(neuronId)); + + if (isGroup) { + groupOrPartOfGroupSet.add(neuronId); + } else if (isPartOfGroup) { + groupOrPartOfGroupSet.add(isPartOfGroup[0]); + } else { + nonGroupOrPartCount++; + } + } + + // Enable grouping if there are neurons not in any group and at most one group or part of a group is selected. + return nonGroupOrPartCount > 0 && groupOrPartOfGroupSet.size <= 1; + }, [selectedNeurons, workspace.neuronGroups]); const ungroupEnabled = useMemo(() => { - return Array.from(workspace.selectedNeurons).some((neuronId) => workspace.neuronGroups[neuronId]); - }, [workspace.selectedNeurons, workspace.neuronGroups]); + return selectedNeurons.some((neuronId) => { + // Check if the neuronId is a group itself + const isGroup = Boolean(workspace.neuronGroups[neuronId]); + + // Check if the neuronId is part of any group + const isPartOfGroup = Object.values(workspace.neuronGroups).some((group) => group.neurons.has(neuronId)); + + // Enable ungroup if the neuron is a group or is part of a group + return isGroup || isPartOfGroup; + }); + }, [selectedNeurons, workspace.neuronGroups]); const splitEnabled = useMemo(() => { - return Array.from(workspace.selectedNeurons).some((neuronId) => { + return selectedNeurons.some((neuronId) => { const neuron = workspace.availableNeurons[neuronId]; return neuron && neuron.name === neuron.nclass; }); - }, [workspace.selectedNeurons, workspace.availableNeurons]); + }, [selectedNeurons, workspace.availableNeurons]); const joinEnabled = useMemo(() => { - return Array.from(workspace.selectedNeurons).some((neuronId) => { + return selectedNeurons.some((neuronId) => { const neuron = workspace.availableNeurons[neuronId]; return neuron && neuron.name !== neuron.nclass; }); - }, [workspace.selectedNeurons, workspace.availableNeurons]); + }, [selectedNeurons, workspace.availableNeurons]); + + const openGroupEnabled = useMemo(() => { + return selectedNeurons.some((neuronId) => workspace.neuronGroups[neuronId] && !openGroups.has(neuronId)); + }, [selectedNeurons, workspace.neuronGroups, openGroups]); + const closeGroupEnabled = useMemo(() => { + return selectedNeurons.some((neuronId) => workspace.neuronGroups[neuronId] && openGroups.has(neuronId)); + }, [selectedNeurons, workspace.neuronGroups, openGroups]); const handleContextMenu = (event: React.MouseEvent) => { event.preventDefault(); // Prevent default context menu }; + useEffect(() => { + if (open) { + setSubmenuAnchorEl(null); + } + }, [open]); + return ( - Hide - - Group - - - Ungroup + + + Hide - - Join Left-Right + + + Add to Workspace - - Split Left-Right + {joinEnabled && ( + + + Join Left-Right + + )} + {splitEnabled && ( + + + Split Left-Right + + )} + + + {groupEnabled && ( + + + Group + + )} + {ungroupEnabled && ( + + + Ungroup + + )} + {openGroupEnabled && ( + + + Open Group + + )} + {closeGroupEnabled && ( + + + Close Group + + )} + + + + Align + + - Add to Workspace + + handleAlignOption(Alignment.Left)}> + + Align left + + handleAlignOption(Alignment.Right)}> + + Align right + + handleAlignOption(Alignment.Top)}> + + Align top + + handleAlignOption(Alignment.Bottom)}> + + Align bottom + + + handleDistributeOption(Alignment.Horizontal)}> + + Distribute horizontally + + handleDistributeOption(Alignment.Vertical)}> + + Distribute vertically + + ); }; diff --git a/applications/visualizer/frontend/src/components/viewers/TwoD/TwoDLegend.tsx b/applications/visualizer/frontend/src/components/viewers/TwoD/TwoDLegend.tsx index 0b4d1626..862a3539 100644 --- a/applications/visualizer/frontend/src/components/viewers/TwoD/TwoDLegend.tsx +++ b/applications/visualizer/frontend/src/components/viewers/TwoD/TwoDLegend.tsx @@ -1,7 +1,7 @@ import { Box, Divider, IconButton, Typography } from "@mui/material"; import type React from "react"; import { type ColoringOptions, getColorMap, legendNodeNameMapping } from "../../../helpers/twoD/coloringHelper"; -import { LegendType, connectionsLegend, annotationLegend } from "../../../settings/twoDSettings"; +import { LegendType, annotationLegend, connectionsLegend } from "../../../settings/twoDSettings"; import { vars } from "../../../theme/variables"; const { gray100 } = vars; @@ -11,6 +11,7 @@ interface LegendNodeProps { color: string; onClick: () => void; highlighted: boolean; + shape: string; } interface LegendConnectionProps { @@ -20,7 +21,7 @@ interface LegendConnectionProps { highlighted: boolean; } -const LegendNode: React.FC = ({ name, color, onClick, highlighted }) => ( +const LegendNode: React.FC = ({ name, color, onClick, highlighted, shape }) => ( = ({ name, color, onClick, highlight }} onClick={onClick} > - + {name} ); @@ -50,6 +59,7 @@ const LegendConnection: React.FC = ({ name, icon, onClick = ({ coloringOption, setLegendHighlights sx={{ padding: "1rem", borderRadius: "0.5rem", - backgroundColor: "rgba(245, 245, 244, 0.80)", - backdropFilter: "blur(20px)", + backgroundColor: "transparent", }} > {Object.entries(colorMap).map(([name, color]) => ( @@ -103,6 +112,7 @@ const TwoDLegend: React.FC = ({ coloringOption, setLegendHighlights color={color} onClick={() => handleLegendClick(LegendType.Node, name)} highlighted={!nodeHighlight || legendHighlights.get(LegendType.Node) === name} + shape={name === "muscle" || name === "others" ? "rectangle" : "circle"} /> ))} diff --git a/applications/visualizer/frontend/src/components/viewers/TwoD/TwoDMenu.tsx b/applications/visualizer/frontend/src/components/viewers/TwoD/TwoDMenu.tsx index f9ebe402..bca8d9c1 100644 --- a/applications/visualizer/frontend/src/components/viewers/TwoD/TwoDMenu.tsx +++ b/applications/visualizer/frontend/src/components/viewers/TwoD/TwoDMenu.tsx @@ -1,15 +1,17 @@ import { GetAppOutlined, HomeOutlined, TuneOutlined, VisibilityOutlined } from "@mui/icons-material"; import ZoomInIcon from "@mui/icons-material/ZoomIn"; import ZoomOutIcon from "@mui/icons-material/ZoomOut"; -import { Box, Divider, FormControlLabel, FormGroup, IconButton, Popover, ToggleButton, ToggleButtonGroup, Tooltip, Typography } from "@mui/material"; +import { Box, Divider, FormControlLabel, FormGroup, IconButton, Popover, Switch, ToggleButton, ToggleButtonGroup, Tooltip, Typography } from "@mui/material"; import { useState } from "react"; import { ColoringOptions } from "../../../helpers/twoD/coloringHelper.ts"; +import { downloadConnectivityViewer } from "../../../helpers/twoD/downloadHelper.ts"; +import { applyLayout } from "../../../helpers/twoD/twoDHelpers.ts"; +import { useSelectedWorkspace } from "../../../hooks/useSelectedWorkspace.ts"; +import { ViewerType, Visibility } from "../../../models"; import { GRAPH_LAYOUTS, ZOOM_DELTA } from "../../../settings/twoDSettings.tsx"; import { vars } from "../../../theme/variables.ts"; import CustomSwitch from "../../ViewerContainer/CustomSwitch.tsx"; import QuantityInput from "./NumberInput.tsx"; -import { useSelectedWorkspace } from "../../../hooks/useSelectedWorkspace.ts"; -import { applyLayout } from "../../../helpers/twoD/twoDHelpers.ts"; const { gray500 } = vars; @@ -35,7 +37,8 @@ const TwoDMenu = ({ setIncludePostEmbryonic, }) => { const workspace = useSelectedWorkspace(); - const [anchorEl, setAnchorEl] = useState(null); + const [settingsAnchorEl, setSettingsAnchorEl] = useState(null); + const [visibilityAnchorEl, setVisibilityAnchorEl] = useState(null); const onZoomIn = () => { if (!cy) { @@ -66,40 +69,53 @@ const TwoDMenu = ({ if (!cy) { return; } - applyLayout(cy, layout); + applyLayout(cy, layout as GRAPH_LAYOUTS); }; const handleOpenSettings = (event) => { - setAnchorEl(event.currentTarget); + setSettingsAnchorEl(event.currentTarget); }; const handleCloseSettings = () => { - setAnchorEl(null); + setSettingsAnchorEl(null); }; - const handleDownloadClick = () => { - if (!cy) return; + const handleDownloadClick = async () => { + await downloadConnectivityViewer(cy, workspace.name); + }; - const pngDataUrl = cy.png({ - output: "base64uri", - bg: "white", - full: true, - scale: 2, - }); + const handleOpenVisibility = (event) => { + setVisibilityAnchorEl(event.currentTarget); + }; - // Create a link element - const link = document.createElement("a"); - link.href = pngDataUrl; - link.download = `${workspace.name}.png`; + const handleCloseVisibility = () => { + setVisibilityAnchorEl(null); + }; - // Programmatically trigger a click event to download the image - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + const handleToggleVisibility = (neuronId) => { + workspace.customUpdate((draft) => { + const neuron = draft.visibilities[neuronId]; + if (neuron) { + const currentVisibility = neuron[ViewerType.Graph]?.visibility; + neuron[ViewerType.Graph].visibility = currentVisibility === Visibility.Visible ? Visibility.Hidden : Visibility.Visible; + draft.removeSelection(neuronId, ViewerType.Graph); + } + }); }; - const open = Boolean(anchorEl); - const id = open ? "settings-popover" : undefined; + const openSettings = Boolean(settingsAnchorEl); + const settingsId = openSettings ? "settings-popover" : undefined; + + const openVisibility = Boolean(visibilityAnchorEl); + const visibilityId = openVisibility ? "visibility-popover" : undefined; + + const visibleNeurons = Object.entries(workspace.visibilities) + .filter(([_, data]) => data[ViewerType.Graph].visibility === Visibility.Visible) + .map(([key, _]) => key); + + const hiddenNeurons = Object.entries(workspace.visibilities) + .filter(([_, data]) => data[ViewerType.Graph].visibility === Visibility.Hidden) + .map(([key, _]) => key); return ( { if (newColoringOption !== null) { - // Prevent deselecting all options onColoringOptionChange(newColoringOption); } }} @@ -193,14 +208,7 @@ const TwoDMenu = ({ Neuroconnectors have at least: - + Chemical Synapses @@ -240,7 +248,6 @@ const TwoDMenu = ({ exclusive onChange={(_, newLayout) => { if (newLayout !== null) { - // Prevent deselecting all options onLayoutChange(newLayout); } }} @@ -298,11 +305,70 @@ const TwoDMenu = ({ - - + + + + + Visible Neurons + + + {visibleNeurons.map((neuronId) => ( + handleToggleVisibility(neuronId)} + /> + } + label={neuronId} + /> + ))} + + + + Hidden Neurons + + + {hiddenNeurons.map((neuronId) => ( + handleToggleVisibility(neuronId)} + /> + } + label={neuronId} + /> + ))} + + diff --git a/applications/visualizer/frontend/src/components/viewers/TwoD/TwoDViewer.tsx b/applications/visualizer/frontend/src/components/viewers/TwoD/TwoDViewer.tsx index 11affa32..b1068cc5 100644 --- a/applications/visualizer/frontend/src/components/viewers/TwoD/TwoDViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/TwoD/TwoDViewer.tsx @@ -1,34 +1,50 @@ -import { useState, useEffect, useRef } from "react"; +import { Box, Snackbar } from "@mui/material"; import cytoscape, { type Core, type EventHandler } from "cytoscape"; -import fcose from "cytoscape-fcose"; import dagre from "cytoscape-dagre"; +import fcose from "cytoscape-fcose"; +import { debounce } from "lodash"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { useGlobalContext } from "../../../contexts/GlobalContext.tsx"; +import { ColoringOptions, getColor } from "../../../helpers/twoD/coloringHelper"; +import { computeGraphDifferences, updateHighlighted, updateParallelEdges, updateParentNodes } from "../../../helpers/twoD/graphRendering.ts"; +import { + applyLayout, + getHiddenNeuronsIn2D, + getVisibleActiveNeuronsIn2D, + isNeuronPartOfClosedGroup, + refreshLayout, + updateWorkspaceNeurons2DViewerData, +} from "../../../helpers/twoD/twoDHelpers"; +import { areSetsEqual } from "../../../helpers/utils.ts"; import { useSelectedWorkspace } from "../../../hooks/useSelectedWorkspace"; +import { ViewerType } from "../../../models"; +import { GlobalError } from "../../../models/Error.ts"; import { type Connection, ConnectivityService } from "../../../rest"; -import { GRAPH_STYLES } from "../../../theme/twoDStyles"; -import { applyLayout, refreshLayout, updateWorkspaceNeurons2DViewerData } from "../../../helpers/twoD/twoDHelpers"; import { CHEMICAL_THRESHOLD, ELECTRICAL_THRESHOLD, + FOCUS_CLASS, GRAPH_LAYOUTS, - type LegendType, + HOVER_CLASS, INCLUDE_ANNOTATIONS, - INCLUDE_NEIGHBORING_CELLS, INCLUDE_LABELS, + INCLUDE_NEIGHBORING_CELLS, INCLUDE_POST_EMBRYONIC, + type LegendType, + SELECTED_CLASS, + SHOW_EDGE_LABEL_CLASS, } from "../../../settings/twoDSettings"; -import TwoDMenu from "./TwoDMenu"; -import TwoDLegend from "./TwoDLegend"; -import { Box } from "@mui/material"; -import { ColoringOptions, getColor } from "../../../helpers/twoD/coloringHelper"; +import { GRAPH_STYLES } from "../../../theme/twoDStyles"; import ContextMenu from "./ContextMenu"; -import { computeGraphDifferences, updateHighlighted } from "../../../helpers/twoD/graphRendering.ts"; -import { areSetsEqual } from "../../../helpers/utils.ts"; +import TwoDLegend from "./TwoDLegend"; +import TwoDMenu from "./TwoDMenu"; cytoscape.use(fcose); cytoscape.use(dagre); const TwoDViewer = () => { const workspace = useSelectedWorkspace(); + const { handleErrors } = useGlobalContext(); const cyContainer = useRef(null); const cyRef = useRef(null); const [connections, setConnections] = useState([]); @@ -47,12 +63,41 @@ const TwoDViewer = () => { const [includePostEmbryonic, setIncludePostEmbryonic] = useState(INCLUDE_POST_EMBRYONIC); const [mousePosition, setMousePosition] = useState<{ mouseX: number; mouseY: number } | null>(null); const [legendHighlights, setLegendHighlights] = useState>(new Map()); - const [hiddenNodes, setHiddenNodes] = useState>(new Set()); + const [openGroups, setOpenGroups] = useState>(new Set()); + + const [missingNeuronsState, setMissingNeuronsState] = useState({ + reportedNeurons: new Set(), + unreportedNeurons: new Set(), + }); + + const selectedNeurons = workspace.getViewerSelecedNeurons(ViewerType.Graph); + + const visibleActiveNeurons = useMemo(() => { + return getVisibleActiveNeuronsIn2D(workspace); + }, [ + Array.from(workspace.activeNeurons) + .map((neuronId) => workspace.visibilities[neuronId]?.[ViewerType.Graph]?.visibility || "") + .join(","), + ]); + + const hiddenNeurons = useMemo(() => { + return getHiddenNeuronsIn2D(workspace); + }, [ + Object.keys(workspace.availableNeurons) + .map((neuronId) => workspace.visibilities[neuronId]?.[ViewerType.Graph]?.visibility || "") + .join(","), + ]); const handleContextMenuClose = () => { setMousePosition(null); }; + const handleCloseSnackbar = () => { + setMissingNeuronsState((prevState) => ({ + reportedNeurons: new Set([...prevState.reportedNeurons, ...prevState.unreportedNeurons]), + unreportedNeurons: new Set(), + })); + }; // Initialize and update Cytoscape useEffect(() => { if (!cyContainer.current) return; @@ -63,6 +108,9 @@ const TwoDViewer = () => { layout: { name: layout, }, + boxSelectionEnabled: true, + motionBlur: true, + selectionType: "additive", }); cyRef.current = cy; @@ -89,8 +137,8 @@ const TwoDViewer = () => { useEffect(() => { if (!workspace) return; - // Convert activeNeurons and activeDatasets to comma-separated strings - const cells = Array.from(workspace.activeNeurons || []).join(","); + // Convert visibleActiveNeurons and activeDatasets to comma-separated strings + const cells = Array.from(visibleActiveNeurons || []).join(","); const datasetIds = Object.values(workspace.activeDatasets) .map((dataset) => dataset.id) .join(","); @@ -110,12 +158,12 @@ const TwoDViewer = () => { .then((connections) => { setConnections(connections); }) - .catch((error) => { - console.error("Failed to fetch connections:", error); + .catch(() => { + handleErrors(new GlobalError("Failed to fetch connections")); }); }, [ workspace.activeDatasets, - workspace.activeNeurons, + visibleActiveNeurons, includeNeighboringCells, includeNeighboringCellsAsIndividualCells, includeAnnotations, @@ -128,7 +176,7 @@ const TwoDViewer = () => { if (cyRef.current) { updateGraphElements(cyRef.current, connections); } - }, [connections, hiddenNodes, workspace.neuronGroups, includePostEmbryonic, splitJoinState]); + }, [connections, hiddenNeurons, workspace.neuronGroups, includePostEmbryonic, splitJoinState, openGroups]); useEffect(() => { if (cyRef.current) { @@ -138,15 +186,61 @@ const TwoDViewer = () => { useEffect(() => { if (cyRef.current) { - updateHighlighted(cyRef.current, Array.from(workspace.activeNeurons), Array.from(workspace.selectedNeurons), legendHighlights, workspace.neuronGroups); + updateHighlighted(cyRef.current, Array.from(visibleActiveNeurons), selectedNeurons, legendHighlights); } - }, [legendHighlights, workspace.selectedNeurons, workspace.neuronGroups]); + }, [legendHighlights, selectedNeurons, workspace.neuronGroups]); // Update layout when layout setting changes useEffect(() => { updateLayout(); }, [layout, connections]); + const correctGjSegments = (edgeSel = '[type="electrical"]') => { + const cy = cyRef.current; + if (!cy) return; + + const edges = cy.edges(edgeSel); + const disFactors = [-2.0, -1.5, -0.5, 0.5, 1.5, 2.0]; + + cy.startBatch(); + + for (const e of edges) { + const sourcePos = e.source().position(); + const targetPos = e.target().position(); + + const length = Math.sqrt((targetPos.x - sourcePos.x) ** 2 + (targetPos.y - sourcePos.y) ** 2); + + const divider = (length > 60 ? 7 : length > 40 ? 5 : 3) / length; + + const segweights = disFactors.map((d) => 0.5 + d * divider).join(" "); + + if (e.style("segment-weights") !== segweights) { + e.style({ "segment-weights": segweights }); + } + } + + cy.endBatch(); + }; + + useEffect(() => { + if (!cyRef.current) return; + + const cy = cyRef.current; + + const debouncedCorrectGjSegments = debounce(() => { + correctGjSegments(); + }, 100); + + cy.on("position", "node", debouncedCorrectGjSegments); + cy.on("layoutstop", debouncedCorrectGjSegments); + + return () => { + cy.off("position", "node", debouncedCorrectGjSegments); + cy.off("layoutstop", debouncedCorrectGjSegments); + debouncedCorrectGjSegments.cancel(); + }; + }, []); + // Add event listener for node clicks to toggle neuron selection and right-click context menu useEffect(() => { if (!cyRef.current) return; @@ -155,20 +249,22 @@ const TwoDViewer = () => { const handleNodeClick = (event) => { const neuronId = event.target.id(); - const isSelected = workspace.selectedNeurons.has(neuronId); - workspace.toggleSelectedNeuron(neuronId); + const selectedNeurons = workspace.getSelection(ViewerType.Graph); + const isSelected = selectedNeurons.includes(neuronId); if (isSelected) { - event.target.removeClass("selected"); + workspace.removeSelection(neuronId, ViewerType.Graph); + event.target.removeClass(SELECTED_CLASS); } else { - event.target.addClass("selected"); + workspace.addSelection(neuronId, ViewerType.Graph); + event.target.addClass(SELECTED_CLASS); } }; const handleBackgroundClick = (event) => { if (event.target === cy) { - workspace.clearSelectedNeurons(); - cy.nodes(".selected").removeClass("selected"); + workspace.clearSelection(ViewerType.Graph); + cy.nodes(`.${SELECTED_CLASS}`).removeClass(SELECTED_CLASS); setLegendHighlights(new Map()); // Reset legend highlights } @@ -179,8 +275,8 @@ const TwoDViewer = () => { const cyEvent = event as any; // Cast to any to access originalEvent const originalEvent = cyEvent.originalEvent as MouseEvent; - - if (workspace.selectedNeurons.size > 0) { + const selectedNeurons = workspace.getViewerSelecedNeurons(ViewerType.Graph); + if (selectedNeurons.length > 0) { setMousePosition({ mouseX: originalEvent.clientX, mouseY: originalEvent.clientY, @@ -191,16 +287,18 @@ const TwoDViewer = () => { }; const handleEdgeMouseOver = (event) => { - event.target.addClass("hover"); + event.target.addClass(HOVER_CLASS); }; const handleEdgeMouseOut = (event) => { - event.target.removeClass("hover"); - event.target.removeClass("focus"); + event.target.removeClass(HOVER_CLASS); + event.target.removeClass(FOCUS_CLASS); + event.target.removeClass(SHOW_EDGE_LABEL_CLASS); }; const handleEdgeFocus = (event) => { - event.target.addClass("focus"); + event.target.toggleClass(SHOW_EDGE_LABEL_CLASS); + event.target.toggleClass(FOCUS_CLASS); }; cy.on("tap", "node", handleNodeClick); @@ -222,34 +320,34 @@ const TwoDViewer = () => { // Update active neurons when split or join state changes useEffect(() => { - const activeNeurons = new Set(workspace.activeNeurons); + const nextActiveNeurons = new Set(workspace.activeNeurons); - splitJoinState.split.forEach((neuronId) => { + for (const neuronId of splitJoinState.split) { if (workspace.activeNeurons.has(neuronId)) { - activeNeurons.delete(neuronId); - Object.values(workspace.availableNeurons).forEach((neuron) => { + nextActiveNeurons.delete(neuronId); + for (const neuron of Object.values(workspace.availableNeurons)) { if (neuron.nclass === neuronId && neuron.name !== neuron.nclass) { - activeNeurons.add(neuron.name); + nextActiveNeurons.add(neuron.name); } - }); + } } - }); + } - splitJoinState.join.forEach((neuronId) => { + for (const neuronId of splitJoinState.join) { const neuronClass = workspace.availableNeurons[neuronId].nclass; if (workspace.activeNeurons.has(neuronId)) { - activeNeurons.delete(neuronId); - Object.values(workspace.availableNeurons).forEach((neuron) => { + nextActiveNeurons.delete(neuronId); + for (const neuron of Object.values(workspace.availableNeurons)) { if (neuron.nclass === neuronClass) { - activeNeurons.delete(neuron.name); + nextActiveNeurons.delete(neuron.name); } - }); - activeNeurons.add(neuronClass); + } + nextActiveNeurons.add(neuronClass); } - }); + } - if (!areSetsEqual(activeNeurons, workspace.activeNeurons)) { - workspace.setActiveNeurons(activeNeurons); + if (!areSetsEqual(nextActiveNeurons, workspace.activeNeurons)) { + workspace.setActiveNeurons(nextActiveNeurons); } }, [splitJoinState, workspace.id]); @@ -258,13 +356,13 @@ const TwoDViewer = () => { if (!cyRef.current) return; cyRef.current.batch(() => { - cyRef.current.edges().forEach((edge) => { + for (const edge of cyRef.current.edges()) { if (includeLabels) { edge.addClass("showEdgeLabel"); } else { edge.removeClass("showEdgeLabel"); } - }); + } }); }, [includeLabels]); @@ -274,27 +372,65 @@ const TwoDViewer = () => { connections, workspace, splitJoinState, - hiddenNodes, + hiddenNeurons, + openGroups, includeNeighboringCellsAsIndividualCells, includeAnnotations, includePostEmbryonic, ); cy.batch(() => { - cy.remove(nodesToRemove); - cy.remove(edgesToRemove); cy.add(nodesToAdd); cy.add(edgesToAdd); + updateParentNodes(cy, workspace, openGroups); + cy.remove(nodesToRemove); + cy.remove(edgesToRemove); + updateParallelEdges(cy); }); updateNodeColors(); - updateHighlighted(cy, Array.from(workspace.activeNeurons), Array.from(workspace.selectedNeurons), legendHighlights, workspace.neuronGroups); + updateHighlighted(cy, Array.from(visibleActiveNeurons), selectedNeurons, legendHighlights); + checkSplitNeuronsInGraph(); + }; + + const checkSplitNeuronsInGraph = () => { + const newMissingNeurons = new Set(); + for (const neuronId of splitJoinState.split) { + const cells = workspace.getNeuronCellsByClass(neuronId); + for (const cellId of cells) { + // Check if the cell is part of a closed group, if not, check if it's missing + if (!isNeuronPartOfClosedGroup(cellId, workspace, openGroups) && !cyRef.current.getElementById(cellId).length) { + newMissingNeurons.add(cellId); + } + } + } + + const { reportedNeurons } = missingNeuronsState; + + // Find the newly missing neurons that haven't been reported yet + const unreportedNeurons = new Set([...newMissingNeurons].filter((neuron) => !reportedNeurons.has(neuron))); + + // Remove neurons from reportedNeurons that are no longer part of the splitJoinState.split + const updatedReportedNeurons = new Set( + [...reportedNeurons].filter((neuron) => { + const nclass = workspace.getNeuronClass(neuron); + return splitJoinState.split.has(nclass); + }), + ); + + // Check if there are any changes + if (!areSetsEqual(missingNeuronsState.unreportedNeurons, unreportedNeurons) || !areSetsEqual(missingNeuronsState.reportedNeurons, updatedReportedNeurons)) { + setMissingNeuronsState({ + unreportedNeurons: unreportedNeurons, + reportedNeurons: updatedReportedNeurons, + }); + } }; const updateLayout = () => { if (cyRef.current) { const cy = cyRef.current; - applyLayout(cy, layout); + applyLayout(cy, layout as GRAPH_LAYOUTS); updateWorkspaceNeurons2DViewerData(workspace, cy); } }; @@ -303,23 +439,26 @@ const TwoDViewer = () => { if (!cyRef.current) { return; } - cyRef.current.nodes().forEach((node) => { + for (const node of cyRef.current.nodes()) { + if (node.hasClass("groupNode")) { + return; + } const nodeId = node.id(); const group = workspace.neuronGroups[nodeId]; + let colors = []; if (group) { // If the node is a group, collect colors from all neurons in the group - group.neurons.forEach((neuronId) => { + for (const neuronId of group.neurons) { const neuron = workspace.availableNeurons[neuronId]; if (neuron) { colors = colors.concat(getColor(neuron, coloringOption)); } - }); + } // Ensure unique colors are used - const uniqueColors = [...new Set(colors)]; - colors = uniqueColors; + colors = [...new Set(colors)]; } else { const neuron = workspace.availableNeurons[nodeId]; if (neuron == null) { @@ -328,13 +467,16 @@ const TwoDViewer = () => { } colors = getColor(neuron, coloringOption); } - - colors.forEach((color, index) => { - node.style(`pie-${index + 1}-background-color`, color); - node.style(`pie-${index + 1}-background-size`, 100 / colors.length); // Equal size for each slice - }); - node.style("pie-background-opacity", 1); - }); + if (colors.length > 1 && node.style("shape") === "ellipse") { + colors.forEach((color, index) => { + node.style(`pie-${index + 1}-background-color`, color); + node.style(`pie-${index + 1}-background-size`, 100 / colors.length); + }); + node.style("pie-background-opacity", 1); + } else { + node.style("background-color", colors[0]); + } + } }; return ( @@ -360,7 +502,7 @@ const TwoDViewer = () => { includePostEmbryonic={includePostEmbryonic} setIncludePostEmbryonic={setIncludePostEmbryonic} /> - + { onClose={handleContextMenuClose} position={mousePosition} setSplitJoinState={setSplitJoinState} - setHiddenNodes={setHiddenNodes} + openGroups={openGroups} + setOpenGroups={setOpenGroups} + cy={cyRef.current} + /> + 0} + onClose={handleCloseSnackbar} + message={`Warning: The following neurons are missing from the graph due to the threshold filters: + ${Array.from(missingNeuronsState.unreportedNeurons).join(", ")}`} + autoHideDuration={6000} /> ); diff --git a/applications/visualizer/frontend/src/components/wrappers/Compare.tsx b/applications/visualizer/frontend/src/components/wrappers/Compare.tsx index 68118a57..b57c07ab 100644 --- a/applications/visualizer/frontend/src/components/wrappers/Compare.tsx +++ b/applications/visualizer/frontend/src/components/wrappers/Compare.tsx @@ -3,6 +3,7 @@ import { type Theme, ThemeProvider } from "@mui/material/styles"; import { Suspense } from "react"; import "@metacell/geppetto-meta-ui/flex-layout/style/light.scss"; import { Box } from "@mui/system"; +import { useGlobalContext } from "../../contexts/GlobalContext.tsx"; import { DownloadIcon, LinkIcon } from "../../icons"; import theme from "../../theme"; import { vars } from "../../theme/variables.ts"; @@ -11,6 +12,8 @@ const { gray100 } = vars; const drawerWidth = "22.31299rem"; const drawerHeight = "3.5rem"; function CompareWrapper({ children, sidebarOpen }) { + const { serializeGlobalContext } = useGlobalContext(); + return ( <> @@ -73,7 +76,17 @@ function CompareWrapper({ children, sidebarOpen }) { - diff --git a/applications/visualizer/frontend/src/contexts/GlobalContext.tsx b/applications/visualizer/frontend/src/contexts/GlobalContext.tsx index d034feb6..e7fce491 100644 --- a/applications/visualizer/frontend/src/contexts/GlobalContext.tsx +++ b/applications/visualizer/frontend/src/contexts/GlobalContext.tsx @@ -1,8 +1,27 @@ +import { produce } from "immer"; +import pako from "pako"; import type React from "react"; import { type ReactNode, createContext, useContext, useEffect, useState } from "react"; +import ErrorAlert from "../components/ErrorAlert.tsx"; +import ErrorBoundary from "../components/ErrorBoundary.tsx"; import { ViewMode } from "../models"; import { Workspace } from "../models"; +import { GlobalError } from "../models/Error.ts"; import { type Dataset, DatasetsService } from "../rest"; +import type { SerializedGlobalContext } from "./SerializedContext.tsx"; + +function b64Tob64Url(buffer: string): string { + return buffer.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +} + +function b64UrlTo64(value: string): string { + const m = value.length % 4; + return value + .replace(/-/g, "+") + .replace(/_/g, "/") + .padEnd(value.length + (m === 0 ? 0 : 4 - m), "="); +} + export interface GlobalContextType { workspaces: Record; currentWorkspaceId: string | undefined; @@ -16,8 +35,11 @@ export interface GlobalContextType { getCurrentWorkspace: () => Workspace; setSelectedWorkspacesIds: (workspaceId: Set) => void; datasets: Record; - fetchDatasets: () => void; setAllWorkspaces: (workspaces: Record) => void; + handleErrors: (error: Error) => void; + serializeGlobalContext: () => string; + restoreGlobalContext: (context: SerializedGlobalContext) => void; + restoreGlobalContextFromBase64: (base64Context: string) => void; } interface GlobalContextProviderProps { @@ -32,16 +54,17 @@ export const GlobalContextProvider: React.FC = ({ ch const [viewMode, setViewMode] = useState(ViewMode.Default); const [selectedWorkspacesIds, setSelectedWorkspacesIds] = useState>(new Set()); const [datasets, setDatasets] = useState>({}); - + const [openErrorAlert, setOpenErrorAlert] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); const createWorkspace = (id: string, name: string, activeDatasetKeys: Set, activeNeurons: Set) => { // Convert the activeDatasetKeys into a Record const activeDatasets: Record = {}; - activeDatasetKeys.forEach((key) => { + for (const key of activeDatasetKeys) { if (datasets[key]) { activeDatasets[key] = datasets[key]; } - }); + } // Create a new workspace using the activeDatasets record const newWorkspace = new Workspace(id, name, activeDatasets, activeNeurons, updateWorkspace); @@ -70,8 +93,88 @@ export const GlobalContextProvider: React.FC = ({ ch }; const getCurrentWorkspace = () => { - return workspaces[currentWorkspaceId]; + return workspaces?.[currentWorkspaceId]; }; + + const handleErrors = (error: Error) => { + if (error instanceof GlobalError) { + setErrorMessage(error.message); + setOpenErrorAlert(true); + } + }; + + const serializeGlobalContext = () => { + // Create a special context with only the information we need + const subContext = { + workspaces: {}, + currentWorkspaceId, + viewMode, + selectedWorkspacesIds: [...selectedWorkspacesIds], + }; + + // Modify this context to remove the elements we don't want by workspace and to simplify it + // - layoutManager (not serializable) + // - store (related to the layour manager) + // - availableNeurons (they are computed when a workspace is created) + // - activeDatasets, we replace only with the keys of the datasets + // - synchronizerOrchestrator, we move some of its properties to the simplified workspace (gain space) + // - contexts comes from the synchronizer orchestrator + // - active viewers comes from the synchronizer orchestrator + const updatedSubContext = produce(subContext, (draft) => { + const simpleWorkspace = {}; + for (const [key, workspace] of Object.entries(workspaces)) { + const copy = { + ...workspace, + layoutManager: undefined, + store: undefined, + availableNeurons: undefined, + syncOrchestrator: undefined, + activeDatasets: Object.keys(workspace.activeDatasets), + contexts: workspace.syncOrchestrator.contexts, + activeSyncs: Object.fromEntries(workspace.syncOrchestrator.synchronizers.map((sync) => [sync.pair, sync.active])), + }; + simpleWorkspace[key] = copy; + } + draft.workspaces = simpleWorkspace; + }); + + const jsonContext = JSON.stringify(updatedSubContext, (_key, value) => (value instanceof Set ? [...value] : value)); + const gzipContext = pako.gzip(jsonContext); + const base64UrlFragment = btoa(String.fromCharCode.apply(null, gzipContext)); + return b64Tob64Url(base64UrlFragment); + }; + + const restoreGlobalContext = (context: SerializedGlobalContext) => { + setCurrentWorkspaceId(context.currentWorkspaceId); + setSelectedWorkspacesIds(new Set(context.selectedWorkspacesIds)); + setViewMode(context.viewMode); + + const reconstructedWorkspaces = {}; + for (const [wsId, ws] of Object.entries(context.workspaces)) { + const activeDatasets: Record = {}; + for (const key of ws.activeDatasets) { + if (datasets[key]) { + activeDatasets[key] = datasets[key]; + } + } + const workspace = new Workspace(ws.id, ws.name, activeDatasets, new Set(ws.activeNeurons), updateWorkspace, ws.activeSyncs, ws.contexts, ws.visibilities); + workspace.viewers = ws.viewers; + + reconstructedWorkspaces[wsId] = workspace; + } + setWorkspaces(reconstructedWorkspaces); + }; + + const restoreGlobalContextFromBase64 = (base64UrlContext: string) => { + const base64Context = b64UrlTo64(base64UrlContext); + const gzipedContext = Uint8Array.from(atob(base64Context), (c) => c.charCodeAt(0)); + const serializedContext = pako.ungzip(gzipedContext); + const jsonContext = new TextDecoder().decode(serializedContext); + + const ctx = JSON.parse(jsonContext) as SerializedGlobalContext; + restoreGlobalContext(ctx); + }; + const getGlobalContext = () => ({ workspaces, currentWorkspaceId, @@ -84,32 +187,43 @@ export const GlobalContextProvider: React.FC = ({ ch setViewMode, selectedWorkspacesIds, setSelectedWorkspacesIds, - fetchDatasets, datasets, setAllWorkspaces, + handleErrors, + serializeGlobalContext, + restoreGlobalContext, + restoreGlobalContextFromBase64, }); - const fetchDatasets = async () => { - try { - const response = await DatasetsService.getDatasets({}); - const datasetsRecord = response.reduce( - (acc, dataset) => { - acc[dataset.id] = dataset; - return acc; - }, - {} as Record, - ); - - setDatasets(datasetsRecord); - } catch (error) { - console.error("Failed to fetch datasets", error); - } - }; useEffect(() => { + const fetchDatasets = async () => { + try { + const response = await DatasetsService.getDatasets({}); + const datasetsRecord = response.reduce( + (acc, dataset) => { + acc[dataset.id] = dataset; + return acc; + }, + {} as Record, + ); + + setDatasets(datasetsRecord); + } catch (error) { + setOpenErrorAlert(true); + setErrorMessage("Failed to fetch datasets"); + } + }; fetchDatasets(); }, []); - return {children}; + return ( + + + {children} + + + + ); }; export const useGlobalContext = () => { const context = useContext(GlobalContext); diff --git a/applications/visualizer/frontend/src/contexts/GlobalContextReloader.tsx b/applications/visualizer/frontend/src/contexts/GlobalContextReloader.tsx new file mode 100644 index 00000000..b2a980c0 --- /dev/null +++ b/applications/visualizer/frontend/src/contexts/GlobalContextReloader.tsx @@ -0,0 +1,21 @@ +import { useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { useGlobalContext } from "./GlobalContext"; + +const GlobalContextReloader = () => { + const { code } = useParams(); + const navigate = useNavigate(); + const { restoreGlobalContextFromBase64, datasets } = useGlobalContext(); + + // biome-ignore lint/correctness/useExhaustiveDependencies: navigate and restoreGlobalContextFromBase64 are function from global context + useEffect(() => { + if (code && datasets && Object.keys(datasets).length > 0) { + restoreGlobalContextFromBase64(code); + navigate("/"); + } + }, [datasets, code]); + + return
    Loading datasets
    ; +}; + +export default GlobalContextReloader; diff --git a/applications/visualizer/frontend/src/contexts/SerializedContext.tsx b/applications/visualizer/frontend/src/contexts/SerializedContext.tsx new file mode 100644 index 00000000..3fcff481 --- /dev/null +++ b/applications/visualizer/frontend/src/contexts/SerializedContext.tsx @@ -0,0 +1,23 @@ +import type { NeuronGroup, ViewMode, ViewerType } from "../models"; +import type { ViewerData, ViewerSynchronizationPair } from "../models/models"; +import type { SynchronizerContext } from "../models/synchronizer"; + +type SerializedWorkspace = { + id: string; + name: string; + activeNeurons: Array; + selectedNeurons: Array; + activeDatasets: Array; + viewers: Record; + neuronGroups: Record; + contexts: Record; + activeSyncs: Record; + visibilities: Record; +}; + +export type SerializedGlobalContext = { + workspaces: Record; + currentWorkspaceId: string; + viewMode: ViewMode; + selectedWorkspacesIds: Array; +}; diff --git a/applications/visualizer/frontend/src/helpers/slidingRing.ts b/applications/visualizer/frontend/src/helpers/slidingRing.ts index 6c5352c5..05b22ab4 100644 --- a/applications/visualizer/frontend/src/helpers/slidingRing.ts +++ b/applications/visualizer/frontend/src/helpers/slidingRing.ts @@ -76,13 +76,18 @@ export class SlidingRing { onEvict: options.onEvict, }; + this.initRing(options.startAt); + } + + private initRing(at: number) { // initialize ring const halfSize = Math.floor(this.ring.length / 2); - let tailN = options.startAt - halfSize; - let headN = options.startAt + halfSize; + let tailN = at - halfSize; + let headN = at + halfSize; // the ring may start near the extent // this account for that adjustment + const [min, max] = this.extent; if (tailN < min) tailN = min; if (headN > max) { headN = max; @@ -97,11 +102,11 @@ export class SlidingRing { this.ring[i] = { n, o }; } - this.pos = options.startAt - tailN; + this.pos = at - tailN; this.tail = 0; this.head = this.ring.length - 1; - this.cb.onSelected(options.startAt, this.ring[this.pos].o); + this.cb.onSelected(at, this.ring[this.pos].o); } next() { @@ -166,6 +171,14 @@ export class SlidingRing { this.pos = prevPos; } + goto(n: number) { + this.cb.onUnselected(this.ring[this.pos].n, this.ring[this.pos].o); + for (const item of this.ring) { + this.cb.onEvict(item.n, item.o); + } + this.initRing(n); + } + debug() { let text = "["; diff --git a/applications/visualizer/frontend/src/helpers/twoD/alignHelper.ts b/applications/visualizer/frontend/src/helpers/twoD/alignHelper.ts new file mode 100644 index 00000000..0934d05b --- /dev/null +++ b/applications/visualizer/frontend/src/helpers/twoD/alignHelper.ts @@ -0,0 +1,86 @@ +import type { Core } from "cytoscape"; +import { Alignment } from "../../models"; + +export const alignNeurons = (alignment: Alignment, selectedNeurons: string[], cy: Core) => { + // Get Cytoscape elements for selected neurons + const cyNodes = selectedNeurons.map((neuronId) => cy.getElementById(neuronId)); + + if (cyNodes.some((node) => !node || !node.position())) { + console.error("Some selected neurons do not have positions set in Cytoscape."); + return; + } + + const xPositions = cyNodes.map((node) => node.position("x")); + const yPositions = cyNodes.map((node) => node.position("y")); + + let targetX: number | undefined; + let targetY: number | undefined; + + switch (alignment) { + case Alignment.Left: + targetX = Math.min(...xPositions); + break; + case Alignment.Right: + targetX = Math.max(...xPositions); + break; + case Alignment.Top: + targetY = Math.min(...yPositions); + break; + case Alignment.Bottom: + targetY = Math.max(...yPositions); + break; + } + + // Align nodes in Cytoscape + cy.batch(() => { + cyNodes.forEach((node) => { + const currentPos = node.position(); + if (alignment === Alignment.Left || alignment === Alignment.Right) { + node.position({ x: targetX!, y: currentPos.y }); + } else { + node.position({ x: currentPos.x, y: targetY! }); + } + }); + }); +}; + +export const distributeNeurons = (alignment: Alignment, selectedNeurons: string[], cy: Core) => { + if (selectedNeurons.length <= 1) { + return; + } + // Get Cytoscape elements for selected neurons + const cyNodes = selectedNeurons.map((neuronId) => cy.getElementById(neuronId)); + + if (cyNodes.some((node) => !node || !node.position())) { + console.error("Some selected neurons do not have positions set in Cytoscape."); + return; + } + + // Sort nodes by their current position along the axis to distribute + cyNodes.sort((a, b) => { + if (alignment === Alignment.Horizontal) { + return a.position("x") - b.position("x"); + } else { + return a.position("y") - b.position("y"); + } + }); + + // Get the range of positions along the distribution axis + const minPos = alignment === Alignment.Horizontal ? cyNodes[0].position("x") : cyNodes[0].position("y"); + const maxPos = alignment === Alignment.Horizontal ? cyNodes[cyNodes.length - 1].position("x") : cyNodes[cyNodes.length - 1].position("y"); + + // Calculate the spacing + const spacing = (maxPos - minPos) / (cyNodes.length - 1); + + // Distribute nodes + cy.batch(() => { + cyNodes.forEach((node, index) => { + const currentPos = node.position(); + if (alignment === Alignment.Horizontal) { + node.position({ x: minPos + index * spacing, y: currentPos.y }); + } else { + node.position({ x: currentPos.x, y: minPos + index * spacing }); + } + }); + }); +}; diff --git a/applications/visualizer/frontend/src/helpers/twoD/coloringHelper.ts b/applications/visualizer/frontend/src/helpers/twoD/coloringHelper.ts index ab1db5ec..20691190 100644 --- a/applications/visualizer/frontend/src/helpers/twoD/coloringHelper.ts +++ b/applications/visualizer/frontend/src/helpers/twoD/coloringHelper.ts @@ -20,12 +20,12 @@ interface CellDesign { } export const cellConfig: { [key: string]: CellDesign } = { - b: { type: CellType.Muscle, color: "#A8F5A2" }, - u: { type: CellType.Others, color: "#D9D9D9" }, s: { type: CellType.Sensory, color: "#F9CEF9" }, i: { type: CellType.Inter, color: "#FF887A" }, m: { type: CellType.Motor, color: "#B7DAF5" }, n: { type: CellType.Neurosecretory, color: "#F9D77B" }, + b: { type: CellType.Muscle, color: "#A8F5A2" }, + u: { type: CellType.Others, color: "#D9D9D9" }, }; enum NeurotransmitterType { diff --git a/applications/visualizer/frontend/src/helpers/twoD/concentricLayoutHelper.ts b/applications/visualizer/frontend/src/helpers/twoD/concentricLayoutHelper.ts new file mode 100644 index 00000000..f1ae76a2 --- /dev/null +++ b/applications/visualizer/frontend/src/helpers/twoD/concentricLayoutHelper.ts @@ -0,0 +1,71 @@ +import type { Core } from "cytoscape"; + +export function getConcentricLayoutPositions(cy: Core) { + const center = { + x: cy.width() / 2, + y: cy.height() / 2, + }; + + const innerNodes = cy.nodes(".searchedfor"); + const innerNodeIds = innerNodes.map((n) => n.id()).sort(); + + const outerNodes = cy.nodes().not(".searchedfor"); + const edgeTypes: Record = {}; + + outerNodes.forEach((node) => { + const edges = node.edgesWith(innerNodes); + const edgesElectrical = edges.filter('[type="electrical"]'); + const edgesChemical = edges.filter('[type="chemical"]'); + const hasElectrical = edgesElectrical.length > 0; + const isTarget = edgesChemical.sources().contains(innerNodes); + const isSource = edgesChemical.targets().contains(innerNodes); + let idx = 0; + + if (hasElectrical) { + idx = 3 - (isTarget ? 1 : 0) + (isSource ? 1 : 0); + } else { + idx = (isTarget ? 1 : 0) + (isSource ? 5 : 0); + } + + edgeTypes[node.id()] = idx; + }); + + const outerNodeIds = outerNodes + .map((n) => n.id()) + .sort((a, b) => { + if (edgeTypes[a] === edgeTypes[b]) { + return a.localeCompare(b); + } + return edgeTypes[a] - edgeTypes[b]; + }); + + const innerPositions = createCircle(innerNodeIds, center, outerNodes.length > 0); + const outerPositions = createCircle(outerNodeIds, center); + + return Object.assign({}, innerPositions, outerPositions); +} + +function createCircle(nodes: string[], center: { x: number; y: number }, smallCircle = false) { + const positions: Record = {}; + const count = nodes.length; + const dTheta = (2 * Math.PI - (2 * Math.PI) / count) / Math.max(1, count - 1); + let r = Math.ceil(Math.min(center.x * 2, center.y * 2) / 2.5); + + if (smallCircle) { + r /= count < 4 ? 3 : 2; + } + + if (smallCircle && count === 1) { + positions[nodes[0]] = { x: center.x, y: center.y }; + } else { + for (let i = 0; i < count; i++) { + const theta = -i * dTheta; + positions[nodes[i]] = { + x: center.x - r * Math.sin(theta), + y: center.y - r * Math.cos(theta), + }; + } + } + + return positions; +} diff --git a/applications/visualizer/frontend/src/helpers/twoD/downloadHelper.ts b/applications/visualizer/frontend/src/helpers/twoD/downloadHelper.ts new file mode 100644 index 00000000..71438394 --- /dev/null +++ b/applications/visualizer/frontend/src/helpers/twoD/downloadHelper.ts @@ -0,0 +1,57 @@ +import html2canvas from "html2canvas"; + +export const downloadConnectivityViewer = async (cy, name) => { + if (!cy) return; + + // Capture Cytoscape graph as an image + const pngDataUrl = cy.png({ + output: "base64uri", + bg: "white", + full: true, + scale: 2, + }); + + // Create an image element to load the Cytoscape image + const graphImage = new Image(); + graphImage.src = pngDataUrl; + + await new Promise((resolve) => { + graphImage.onload = resolve; + }); + + // Get the actual size of the captured image + const graphWidth = graphImage.width; + const graphHeight = graphImage.height; + + // Capture the legend as an image + const legendElement = document.querySelector("#legend-container"); + const legendCanvas = await html2canvas(legendElement); + + // Create a new canvas to combine the graph and the legend + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); + + // Set the canvas size to fit both the graph and the legend side by side + canvas.width = graphWidth + legendCanvas.width; + canvas.height = Math.max(graphHeight, legendCanvas.height); + + // Fill the canvas with a white background + context.fillStyle = "white"; + context.fillRect(0, 0, canvas.width, canvas.height); + + // Draw the Cytoscape image onto the canvas + context.drawImage(graphImage, 0, 0, graphWidth, graphHeight); + + // Draw the legend to the right of the graph + context.drawImage(legendCanvas, graphWidth, 0); + + // Download the combined image + const combinedDataUrl = canvas.toDataURL("image/png"); + const link = document.createElement("a"); + link.href = combinedDataUrl; + link.download = `${name}.png`; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +}; diff --git a/applications/visualizer/frontend/src/helpers/twoD/graphRendering.ts b/applications/visualizer/frontend/src/helpers/twoD/graphRendering.ts index 343675db..49c364b6 100644 --- a/applications/visualizer/frontend/src/helpers/twoD/graphRendering.ts +++ b/applications/visualizer/frontend/src/helpers/twoD/graphRendering.ts @@ -1,9 +1,19 @@ -import type { Core, ElementDefinition, CollectionReturnValue } from "cytoscape"; -import { calculateMeanPosition, createEdge, createNode, extractNeuronAttributes, getEdgeId, getNclassSet, isNeuronCell, isNeuronClass } from "./twoDHelpers"; +import type { CollectionReturnValue, Core, ElementDefinition } from "cytoscape"; import type { NeuronGroup, Workspace } from "../../models"; import { ViewerType } from "../../models"; import type { Connection } from "../../rest"; -import { LegendType } from "../../settings/twoDSettings.tsx"; +import { FADED_CLASS, LegendType } from "../../settings/twoDSettings.tsx"; +import { + calculateMeanPosition, + createEdge, + createNode, + extractNeuronAttributes, + getEdgeId, + getNclassSet, + getVisibleActiveNeuronsIn2D, + isNeuronCell, + isNeuronClass, +} from "./twoDHelpers"; export const computeGraphDifferences = ( cy: Core, @@ -11,10 +21,14 @@ export const computeGraphDifferences = ( workspace: Workspace, splitJoinState: { split: Set; join: Set }, hiddenNodes: Set, + openGroups: Set, includeNeighboringCellsAsIndividualCells: boolean, includeAnnotations: boolean, includePostEmbryonic: boolean, ) => { + const visibleActiveNeurons = getVisibleActiveNeuronsIn2D(workspace); + const selectedNeurons = workspace.getViewerSelecedNeurons(ViewerType.Graph); + // Current nodes and edges in the Cytoscape instance const currentNodes = new Set(cy.nodes().map((node) => node.id())); const currentEdges = new Set(cy.edges().map((edge) => edge.id())); @@ -30,13 +44,13 @@ export const computeGraphDifferences = ( // Create a map of connections by edgeId const connectionMap = new Map(); - connections.forEach((conn) => { + for (const conn of connections) { const edgeId = getEdgeId(conn, includeAnnotations); connectionMap.set(edgeId, conn); - }); + } - // Compute expected nodes based on workspace.activeNeurons and connections - const filteredActiveNeurons = Array.from(workspace.activeNeurons).filter((neuronId: string) => { + // Compute expected nodes based on visibleActiveNeurons and connections + const filteredActiveNeurons = Array.from(visibleActiveNeurons).filter((neuronId: string) => { const neuron = workspace.availableNeurons[neuronId]; if (!neuron || hiddenNodes.has(neuronId)) { return false; @@ -48,7 +62,7 @@ export const computeGraphDifferences = ( if (neuronId === nclass) { return true; } - return !(workspace.activeNeurons.has(neuronId) && workspace.activeNeurons.has(nclass)); + return !(visibleActiveNeurons.has(neuronId) && visibleActiveNeurons.has(nclass)); }); // Add active neurons to expected nodes @@ -71,12 +85,19 @@ export const computeGraphDifferences = ( } // Apply split and join rules to expected nodes and edges - expectedNodes = applySplitJoinRulesToNodes(expectedNodes, splitJoinState.split, splitJoinState.join, includeNeighboringCellsAsIndividualCells, workspace); + expectedNodes = applySplitJoinRulesToNodes( + expectedNodes, + splitJoinState.split, + splitJoinState.join, + includeNeighboringCellsAsIndividualCells, + workspace, + visibleActiveNeurons, + ); expectedEdges = applySplitJoinRulesToEdges(expectedEdges, expectedNodes, connectionMap); // Replace individual neurons and edges with groups if necessary - expectedNodes = replaceNodesWithGroups(expectedNodes, workspace.neuronGroups, hiddenNodes); - replaceEdgesWithGroups(expectedEdges, workspace.neuronGroups, connectionMap, includeAnnotations); + expectedNodes = applyGroupingRulesToNodes(expectedNodes, workspace.neuronGroups, hiddenNodes, openGroups); + expectedEdges = applyGroupingRulesToEdges(expectedEdges, workspace.neuronGroups, connectionMap, includeAnnotations, openGroups); // Determine nodes to add and remove for (const nodeId of expectedNodes) { @@ -87,17 +108,34 @@ export const computeGraphDifferences = ( const attributes = new Set(); const groupNeurons = Array.from(group.neurons); - groupNeurons.forEach((neuronId) => { + for (const neuronId of groupNeurons) { const neuron = workspace.availableNeurons[neuronId]; - extractNeuronAttributes(neuron).forEach((attr) => attributes.add(attr)); - }); + for (const attr of extractNeuronAttributes(neuron)) { + attributes.add(attr); + } + } const groupPosition = calculateMeanPosition(groupNeurons, workspace); - nodesToAdd.push(createNode(nodeId, workspace.selectedNeurons.has(nodeId), Array.from(attributes), groupPosition)); + nodesToAdd.push( + createNode(nodeId, selectedNeurons.includes(nodeId), Array.from(attributes), groupPosition, true, undefined, workspace.activeNeurons.has(nodeId)), + ); } else { + let parent = undefined; + + // Check if the neuron belongs to an open group + for (const groupId of openGroups) { + if (workspace.neuronGroups[groupId]?.neurons.has(nodeId)) { + parent = groupId; + break; + } + } const neuron = workspace.availableNeurons[nodeId]; const attributes = extractNeuronAttributes(neuron); - const position = neuron.viewerData[ViewerType.Graph]?.defaultPosition ?? null; - nodesToAdd.push(createNode(nodeId, workspace.selectedNeurons.has(nodeId), attributes, position)); + const neuronVisibility = workspace.visibilities[nodeId]; + const position = neuronVisibility?.[ViewerType.Graph]?.defaultPosition ?? null; + nodesToAdd.push(createNode(nodeId, selectedNeurons.includes(nodeId), attributes, position, false, parent, workspace.activeNeurons.has(nodeId))); + if (!(nodeId in workspace.visibilities)) { + workspace.showNeuron(nodeId); + } } } } @@ -112,8 +150,16 @@ export const computeGraphDifferences = ( for (const edgeId of expectedEdges) { if (!currentEdges.has(edgeId)) { const conn = connectionMap.get(edgeId); + const syns = Object.values(conn.synapses).reduce((acc, num) => acc + num, 0); + const meanSyn = syns / Object.values(conn.synapses).length; + let width; + if (conn.type === "chemical") { + width = Math.max(1, 2 * Math.pow(meanSyn, 1 / 4) - 2); + } else { + width = Math.min(4, meanSyn * 0.8); + } if (conn) { - edgesToAdd.push(createEdge(edgeId, conn, workspace, includeAnnotations)); + edgesToAdd.push(createEdge(edgeId, conn, workspace, includeAnnotations, width)); } } } @@ -129,52 +175,82 @@ export const computeGraphDifferences = ( }; // Replace individual neurons with group nodes -const replaceNodesWithGroups = (expectedNodes: Set, neuronGroups: Record, hiddenNodes: Set) => { +const applyGroupingRulesToNodes = ( + expectedNodes: Set, + neuronGroups: Record, + hiddenNodes: Set, + openGroups: Set, +) => { const nodesToAdd = new Set(); const nodesToRemove = new Set(); - expectedNodes.forEach((nodeId) => { + for (const nodeId of expectedNodes) { for (const groupId in neuronGroups) { const group = neuronGroups[groupId]; + if (group.neurons.has(nodeId)) { if (!hiddenNodes.has(groupId)) { nodesToAdd.add(groupId); } - nodesToRemove.add(nodeId); + if (!openGroups.has(groupId)) { + nodesToRemove.add(nodeId); + } } } - }); + } + + // Remove individual nodes if they are replaced by a closed group node + for (const nodeId of nodesToRemove) { + expectedNodes.delete(nodeId); + } + // Add group nodes + for (const nodeId of nodesToAdd) { + expectedNodes.add(nodeId); + } - nodesToRemove.forEach((nodeId) => expectedNodes.delete(nodeId)); - nodesToAdd.forEach((nodeId) => expectedNodes.add(nodeId)); return expectedNodes; }; // Replace edges involving individual neurons with edges involving group nodes -const replaceEdgesWithGroups = ( +const applyGroupingRulesToEdges = ( expectedEdges: Set, neuronGroups: Record, connectionMap: Map, includeAnnotations: boolean, + openGroups: Set, ) => { const edgesToAdd = new Set(); const edgesToRemove = new Set(); const groupedConnections: Map = new Map(); - expectedEdges.forEach((edgeId) => { + for (const edgeId of expectedEdges) { const conn = connectionMap.get(edgeId); if (!conn) return; let newPre = conn.pre; let newPost = conn.post; - for (const groupId in neuronGroups) { - const group = neuronGroups[groupId]; - if (group.neurons.has(conn.pre)) { - newPre = groupId; + // Skip grouping if either pre or post neuron is in an open group + const preInOpenGroup = Array.from(openGroups).some((groupId) => neuronGroups[groupId]?.neurons.has(conn.pre)); + const postInOpenGroup = Array.from(openGroups).some((groupId) => neuronGroups[groupId]?.neurons.has(conn.post)); + + if (!preInOpenGroup) { + for (const groupId in neuronGroups) { + const group = neuronGroups[groupId]; + if (group.neurons.has(conn.pre)) { + newPre = groupId; + break; + } } - if (group.neurons.has(conn.post)) { - newPost = groupId; + } + + if (!postInOpenGroup) { + for (const groupId in neuronGroups) { + const group = neuronGroups[groupId]; + if (group.neurons.has(conn.post)) { + newPost = groupId; + break; + } } } @@ -198,16 +274,22 @@ const replaceEdgesWithGroups = ( if (fullNewEdgeId !== edgeId) { edgesToRemove.add(edgeId); } - }); + } - groupedConnections.forEach((conn) => { + for (const conn of groupedConnections.values()) { const fullNewEdgeId = getEdgeId(conn, includeAnnotations); edgesToAdd.add(fullNewEdgeId); connectionMap.set(fullNewEdgeId, conn); - }); + } - edgesToRemove.forEach((edgeId) => expectedEdges.delete(edgeId)); - edgesToAdd.forEach((edgeId) => expectedEdges.add(edgeId)); + for (const edgeId of edgesToRemove) { + expectedEdges.delete(edgeId); + } + for (const edgeId of edgesToAdd) { + expectedEdges.add(edgeId); + } + + return expectedEdges; }; const getSimpleEdgeId = (pre: string, post: string, type: string): string => { @@ -221,16 +303,22 @@ const applySplitJoinRulesToNodes = ( toJoin: Set, includeNeighboringCellsAsIndividualCells: boolean, workspace: Workspace, + visibleActiveNeurons: Set, ) => { const nodesToRemove = new Set(); - expectedNodes.forEach((nodeId) => { - if (!workspace.activeNeurons.has(nodeId) && shouldRemoveNode(nodeId, toSplit, toJoin, includeNeighboringCellsAsIndividualCells, workspace)) { + for (const nodeId of expectedNodes) { + if ( + !visibleActiveNeurons.has(nodeId) && + shouldRemoveNode(nodeId, toSplit, toJoin, includeNeighboringCellsAsIndividualCells, workspace, visibleActiveNeurons) + ) { nodesToRemove.add(nodeId); } - }); + } - nodesToRemove.forEach((nodeId) => expectedNodes.delete(nodeId)); + for (const nodeId of nodesToRemove) { + expectedNodes.delete(nodeId); + } return expectedNodes; }; @@ -239,7 +327,7 @@ const applySplitJoinRulesToNodes = ( const applySplitJoinRulesToEdges = (expectedEdges: Set, expectedNodes: Set, connectionMap: Map) => { const edgesToRemove = new Set(); - expectedEdges.forEach((edgeId) => { + for (const edgeId of expectedEdges) { const conn = connectionMap.get(edgeId); if (conn) { @@ -250,9 +338,11 @@ const applySplitJoinRulesToEdges = (expectedEdges: Set, expectedNodes: S edgesToRemove.add(edgeId); } } - }); + } - edgesToRemove.forEach((edgeId) => expectedEdges.delete(edgeId)); + for (const edgeId of edgesToRemove) { + expectedEdges.delete(edgeId); + } return expectedEdges; }; @@ -263,8 +353,9 @@ const shouldRemoveNode = ( toJoin: Set, includeNeighboringCellsAsIndividualCells: boolean, workspace: Workspace, + visibleActiveNeurons: Set, ): boolean => { - const isActive = workspace.activeNeurons.has(nodeId); + const isActive = visibleActiveNeurons.has(nodeId); const isClass = isNeuronClass(nodeId, workspace); const isCell = isNeuronCell(nodeId, workspace); const neuron = workspace.availableNeurons[nodeId]; @@ -281,8 +372,19 @@ const shouldRemoveNode = ( return true; } - // 3. Remove individual cells if showing class nodes and the node is not active and it's not a split exception - if (!includeNeighboringCellsAsIndividualCells && isCell && !isActive && !toSplit.has(neuron.nclass)) { + // 3. Remove individual cells if showing class nodes and the node is not active, it's not a split exception, and there's no active neuron in the same class + const isAnyNeuronInClassActive = + !includeNeighboringCellsAsIndividualCells && isCell && workspace.getNeuronCellsByClass(neuron.nclass).some((cellId) => visibleActiveNeurons.has(cellId)); + + if (!includeNeighboringCellsAsIndividualCells && isCell && !isActive && !toSplit.has(neuron.nclass) && !isAnyNeuronInClassActive) { + return true; + } + + // 4. Remove class nodes if showing individual cells and there's an active cell in the same class + const hasActiveCellInClass = + !includeNeighboringCellsAsIndividualCells && isClass && workspace.getNeuronCellsByClass(nodeId).some((cellId) => visibleActiveNeurons.has(cellId)); + + if (!includeNeighboringCellsAsIndividualCells && isClass && hasActiveCellInClass) { return true; } @@ -295,7 +397,7 @@ const shouldRemoveEdge = (pre: string, post: string, expectedNodes: Set) return !expectedNodes.has(pre) || !expectedNodes.has(post); }; -export const updateHighlighted = (cy, inputIds, selectedIds, legendHighlights, neuronGroups) => { +export const updateHighlighted = (cy, inputIds, selectedIds, legendHighlights) => { // Remove all highlights and return if nothing is selected and no legend item activated. cy.elements().removeClass("faded"); if (selectedIds.length === 0 && legendHighlights.size === 0) { @@ -306,30 +408,23 @@ export const updateHighlighted = (cy, inputIds, selectedIds, legendHighlights, n const sourceIds = selectedIds.length ? selectedIds : inputIds; let sourceNodes = cy.collection(); - sourceIds.forEach((id) => { - let node = cy.getElementById(id); + for (const id of sourceIds) { + const node = cy.getElementById(id); - if (node.empty()) { - // If node is not found, search in neuron groups - for (const groupId in neuronGroups) { - const group = neuronGroups[groupId]; - if (group.neurons.has(id)) { - node = cy.getElementById(groupId); - break; - } - } + if (node.isParent()) { + sourceNodes = sourceNodes.union(node.children()); + } else { + sourceNodes = sourceNodes.union(node); } - - sourceNodes = sourceNodes.union(node); - }); + } // Filter network by edges, as set by legend. let edgeSel = "edge"; legendHighlights.forEach((highlight, type) => { if (type === LegendType.Connection) { edgeSel += `[type="${highlight}"]`; - } else if (type == LegendType.Annotation) { - edgeSel += "." + highlight; + } else if (type === LegendType.Annotation) { + edgeSel += `.${highlight}`; } }); @@ -338,7 +433,7 @@ export const updateHighlighted = (cy, inputIds, selectedIds, legendHighlights, n // Filter network by nodes, as set by legend. legendHighlights.forEach((highlight, type) => { if (type === LegendType.Node) { - connectedNodes = connectedNodes.filter("[?" + highlight + "]"); + connectedNodes = connectedNodes.filter(`[?${highlight}]`); } }); @@ -366,5 +461,33 @@ export const updateHighlighted = (cy, inputIds, selectedIds, legendHighlights, n let highlightedEdges = highlightedNodes.edgesWith(highlightedNodes); highlightedEdges = highlightedEdges.filter(edgeSel); - cy.elements().not(highlightedNodes).not(highlightedEdges).addClass("faded"); + cy.elements().not(highlightedNodes).not(highlightedEdges).addClass(FADED_CLASS); +}; + +export const updateParentNodes = (cy: Core, workspace: Workspace, openGroups: Set) => { + // Iterate through each neuron group in the workspace + for (const [groupId, group] of Object.entries(workspace.neuronGroups)) { + const groupIsOpen = openGroups.has(groupId); + + for (const neuronId of group.neurons) { + const cyNode = cy.getElementById(neuronId); + + if (groupIsOpen) { + // If the group is open, the neuron should have the group as its parent + const parentId = cyNode.parent().first().id(); + + if (parentId !== groupId) { + cyNode.move({ parent: groupId }); + } + } + } + } +}; + +export const updateParallelEdges = (cy: Core) => { + // Remove 'parallel' class from all edges + cy.edges().removeClass("parallel"); + + // Add 'parallel' class to parallel edges + cy.edges('[type = "electrical"]').parallelEdges().filter('[type = "chemical"]').addClass("parallel"); }; diff --git a/applications/visualizer/frontend/src/helpers/twoD/groupHelper.ts b/applications/visualizer/frontend/src/helpers/twoD/groupHelper.ts new file mode 100644 index 00000000..c4b9f641 --- /dev/null +++ b/applications/visualizer/frontend/src/helpers/twoD/groupHelper.ts @@ -0,0 +1,64 @@ +import type { Core } from "cytoscape"; +import type { NeuronGroup, Workspace } from "../../models"; +import { SELECTED_CLASS } from "../../settings/twoDSettings.tsx"; + +export const groupNeurons = (selectedNeurons: Set, workspace: Workspace) => { + const newGroupId = `group_${Date.now()}`; + const newGroupNeurons = new Set(); + const groupsToDelete = new Set(); + let originalGroupName = ""; + let originalGroupColor = "#9FEE9A"; // Default color if no group to delete + + // Gather all neurons that should be part of the new group + for (const neuronId of selectedNeurons) { + let isPartOfAnotherGroup = false; + const group = workspace.neuronGroups[neuronId]; + + // Check if the neuronId is a group itself or part of another group + if (group) { + group.neurons.forEach((groupedNeuronId) => { + newGroupNeurons.add(groupedNeuronId); + }); + groupsToDelete.add(neuronId); + originalGroupName = group.name; + originalGroupColor = group.color; + } else { + Object.entries(workspace.neuronGroups).forEach(([groupId, existingGroup]) => { + if (existingGroup.neurons.has(neuronId)) { + existingGroup.neurons.forEach((groupedNeuronId) => { + newGroupNeurons.add(groupedNeuronId); + }); + groupsToDelete.add(groupId); + originalGroupName = existingGroup.name; + originalGroupColor = existingGroup.color; + isPartOfAnotherGroup = true; + } + }); + + // If neuronId is not part of any group, add it directly + if (!isPartOfAnotherGroup) { + newGroupNeurons.add(neuronId); + } + } + } + + // Create the new group with the gathered neurons + const newGroup: NeuronGroup = { + id: newGroupId, + name: originalGroupName || newGroupId, + color: originalGroupColor, + neurons: newGroupNeurons, + }; + + return { newGroupId, newGroup, groupsToDelete }; +}; + +export function removeNodeFromGroup(cy: Core, nodeId: string, setSelected: boolean) { + const cyNode = cy.getElementById(nodeId); + if (cyNode && cyNode.isNode()) { + cyNode.move({ parent: null }); + if (setSelected) { + cyNode.addClass(SELECTED_CLASS); + } + } +} diff --git a/applications/visualizer/frontend/src/helpers/twoD/splitJoinHelper.ts b/applications/visualizer/frontend/src/helpers/twoD/splitJoinHelper.ts new file mode 100644 index 00000000..f07663e8 --- /dev/null +++ b/applications/visualizer/frontend/src/helpers/twoD/splitJoinHelper.ts @@ -0,0 +1,240 @@ +import { ViewerType, type Workspace } from "../../models"; +import { emptyViewerData, type GraphViewerData, Visibility } from "../../models/models.ts"; +import { calculateMeanPosition, calculateSplitPositions, isNeuronCell, isNeuronClass } from "./twoDHelpers.ts"; + +interface SplitJoinState { + split: Set; + join: Set; +} + +export const processNeuronSplit = (workspace: Workspace, splitJoinState: SplitJoinState): SplitJoinState => { + const newSplit = new Set(splitJoinState.split); + const newJoin = new Set(splitJoinState.join); + const selectedNeurons = workspace.getViewerSelecedNeurons(ViewerType.Graph); + + const newSelectedNeurons = new Set(selectedNeurons); + const graphViewDataUpdates: Record> = {}; + + const groupModifications: Record> = {}; + const groupsToDelete = new Set(); + + for (const neuronId of selectedNeurons) { + if (!isNeuronClass(neuronId, workspace)) { + return; + } + + newSplit.add(neuronId); + newSelectedNeurons.delete(neuronId); + + const individualNeurons = workspace.getNeuronCellsByClass(neuronId); + + const basePosition = workspace.visibilities[neuronId][ViewerType.Graph]?.defaultPosition || { + x: 0, + y: 0, + }; + const positions = calculateSplitPositions(individualNeurons, basePosition); + + updateGroupWithSplitNeurons(workspace, neuronId, individualNeurons, groupModifications, groupsToDelete); + + for (const neuronName of individualNeurons) { + newSelectedNeurons.add(neuronName); + graphViewDataUpdates[neuronName] = { + defaultPosition: positions[neuronName], + visibility: Visibility.Visible, + }; + } + + for (const joinNeuronId of newJoin) { + if (workspace.availableNeurons[joinNeuronId].nclass === neuronId) { + newJoin.delete(joinNeuronId); + } + } + + graphViewDataUpdates[neuronId] = { visibility: Visibility.Unset }; + } + + workspace.customUpdate((draft) => { + draft.setSelection(Array.from(newSelectedNeurons), ViewerType.Graph); + + for (const [groupId, neurons] of Object.entries(groupModifications)) { + if (neurons.size === 0) { + delete draft.neuronGroups[groupId]; + } else { + draft.neuronGroups[groupId].neurons = neurons; + } + } + for (const groupId of groupsToDelete) { + delete draft.neuronGroups[groupId]; + } + + for (const [neuronName, update] of Object.entries(graphViewDataUpdates)) { + if (!(neuronName in draft.visibilities)) { + draft.visibilities[neuronName] = emptyViewerData(update.visibility); + } + if (update.defaultPosition !== undefined) { + draft.visibilities[neuronName][ViewerType.Graph].defaultPosition = update.defaultPosition; + } + } + }); + + return { split: newSplit, join: newJoin }; +}; + +export const processNeuronJoin = (workspace: Workspace, splitJoinState: SplitJoinState): SplitJoinState => { + const newJoin = new Set(splitJoinState.join); + const newSplit = new Set(splitJoinState.split); + const selectedNeurons = workspace.getViewerSelecedNeurons(ViewerType.Graph); + + const newSelectedNeurons = new Set(selectedNeurons); + const graphViewDataUpdates: Record> = {}; + + const groupModifications: Record> = {}; + const groupsToDelete = new Set(); + + for (const neuronId of selectedNeurons) { + if (!isNeuronCell(neuronId, workspace)) { + return; + } + const neuronClass = workspace.availableNeurons[neuronId].nclass; + + const individualNeurons = Object.values(workspace.availableNeurons) + .filter((neuron) => neuron.nclass === neuronClass && neuron.name !== neuronClass) + .map((neuron) => neuron.name); + + const classPosition = calculateMeanPosition(individualNeurons, workspace); + + if (!workspace.visibilities[neuronClass][ViewerType.Graph]?.defaultPosition) { + graphViewDataUpdates[neuronClass] = { + defaultPosition: classPosition, + visibility: Visibility.Visible, + }; + } else { + graphViewDataUpdates[neuronClass] = { + ...graphViewDataUpdates[neuronClass], + visibility: Visibility.Visible, + }; + } + + updateGroupWithJoinedNeurons(workspace, neuronId, neuronClass, individualNeurons, groupModifications, groupsToDelete); + + for (const neuronName of individualNeurons) { + newSelectedNeurons.delete(neuronName); + newJoin.add(neuronName); + graphViewDataUpdates[neuronName] = { visibility: Visibility.Unset }; + } + newSelectedNeurons.add(neuronClass); + + for (const splitNeuronId of newSplit) { + if (workspace.availableNeurons[splitNeuronId].nclass === neuronClass) { + newSplit.delete(splitNeuronId); + } + } + } + + workspace.customUpdate((draft) => { + draft.setSelection(Array.from(newSelectedNeurons), ViewerType.Graph); + + for (const [groupId, neurons] of Object.entries(groupModifications)) { + if (neurons.size === 0) { + delete draft.neuronGroups[groupId]; + } else { + draft.neuronGroups[groupId].neurons = neurons; + } + } + for (const groupId of groupsToDelete) { + delete draft.neuronGroups[groupId]; + } + + for (const [neuronName, update] of Object.entries(graphViewDataUpdates)) { + if (!(neuronName in draft.visibilities)) { + draft.visibilities[neuronName] = emptyViewerData(update.visibility); + } + if (update.defaultPosition !== undefined) { + draft.visibilities[neuronName][ViewerType.Graph].defaultPosition = update.defaultPosition; + } + } + }); + + return { split: newSplit, join: newJoin }; +}; + +export const updateGroupWithSplitNeurons = ( + workspace: Workspace, + neuronId: string, + individualNeurons: string[], + groupModifications: Record>, + groupsToDelete: Set, +) => { + for (const groupId of Object.keys(workspace.neuronGroups)) { + const group = workspace.neuronGroups[groupId]; + + if (group.neurons.has(neuronId)) { + // Replace the class neuron with individual neurons in the group + groupModifications[groupId] = new Set(group.neurons); + groupModifications[groupId].delete(neuronId); + for (const indNeuronId of individualNeurons) { + groupModifications[groupId].add(indNeuronId); + } + + // Remove individual neurons from any other groups + for (const otherGroupId of Object.keys(workspace.neuronGroups)) { + if (otherGroupId !== groupId) { + const otherGroup = workspace.neuronGroups[otherGroupId]; + for (const indNeuronId of individualNeurons) { + if (otherGroup.neurons.has(indNeuronId)) { + if (!groupModifications[otherGroupId]) { + groupModifications[otherGroupId] = new Set(otherGroup.neurons); + } + groupModifications[otherGroupId].delete(indNeuronId); + if (groupModifications[otherGroupId].size === 0) { + groupsToDelete.add(otherGroupId); + } + } + } + } + } + } + } +}; + +export const updateGroupWithJoinedNeurons = ( + workspace: Workspace, + neuronId: string, + neuronClass: string, + individualNeurons: string[], + groupModifications: Record>, + groupsToDelete: Set, +) => { + // If the neuronId (cell) is part of a group, update the group + for (const groupId of Object.keys(workspace.neuronGroups)) { + const group = workspace.neuronGroups[groupId]; + + if (group.neurons.has(neuronId)) { + // Add the class neuron to the group of the selected cell + if (!groupModifications[groupId]) { + groupModifications[groupId] = new Set(group.neurons); + } + groupModifications[groupId].add(neuronClass); + + // Remove individual neurons from any groups they belong to + for (const neuronName of individualNeurons) { + for (const otherGroupId of Object.keys(workspace.neuronGroups)) { + if (workspace.neuronGroups[otherGroupId].neurons.has(neuronName)) { + if (!groupModifications[otherGroupId]) { + groupModifications[otherGroupId] = new Set(workspace.neuronGroups[otherGroupId].neurons); + } + groupModifications[otherGroupId].delete(neuronName); + if (groupModifications[otherGroupId].size === 0) { + groupsToDelete.add(otherGroupId); + } + } + } + } + + // Remove the individual neurons from the group + for (const neuronName of individualNeurons) { + groupModifications[groupId].delete(neuronName); + } + } + } +}; diff --git a/applications/visualizer/frontend/src/helpers/twoD/twoDHelpers.ts b/applications/visualizer/frontend/src/helpers/twoD/twoDHelpers.ts index e48a9458..887178b6 100644 --- a/applications/visualizer/frontend/src/helpers/twoD/twoDHelpers.ts +++ b/applications/visualizer/frontend/src/helpers/twoD/twoDHelpers.ts @@ -1,31 +1,27 @@ import type { Core, ElementDefinition, Position } from "cytoscape"; +import { ViewerType, Visibility, type Workspace } from "../../models"; import type { Connection } from "../../rest"; -import type { Workspace } from "../../models"; -import { ViewerType } from "../../models"; - -import { annotationLegend } from "../../settings/twoDSettings.tsx"; +import { GRAPH_LAYOUTS, LAYOUT_OPTIONS, annotationLegend } from "../../settings/twoDSettings.tsx"; import { cellConfig, neurotransmitterConfig } from "./coloringHelper.ts"; +import { emptyViewerData } from "../../models/models.ts"; +import { getConcentricLayoutPositions } from "./concentricLayoutHelper.ts"; -export const createEdge = (id: string, conn: Connection, workspace: Workspace, includeAnnotations: boolean): ElementDefinition => { +export const createEdge = (id: string, conn: Connection, workspace: Workspace, includeAnnotations: boolean, width: number): ElementDefinition => { const synapses = conn.synapses || {}; const annotations = conn.annotations || []; const label = createEdgeLabel(workspace, synapses); const longLabel = createEdgeLongLabel(workspace, synapses); - let annotationClasses: string[] = []; + const annotationClasses: string[] = annotations.map((annotation) => annotationLegend[annotation]?.id).filter(Boolean); - if (includeAnnotations) { - annotationClasses = annotations.map((annotation) => annotationLegend[annotation]?.id).filter(Boolean); - if (annotationClasses.length === 0) { - annotationClasses.push(annotationLegend.notClassified.id); - } - } else { - annotationClasses.push(conn.type); + if (includeAnnotations && annotationClasses.length === 0) { + annotationClasses.push(annotationLegend.notClassified.id); } - const classes = annotationClasses.join(" "); + annotationClasses.push(conn.type); + const classes = annotationClasses.join(" "); return { group: "edges", data: { @@ -35,6 +31,7 @@ export const createEdge = (id: string, conn: Connection, workspace: Workspace, i label: label, longLabel: longLabel, type: conn.type, + width, }, classes: classes, }; @@ -56,11 +53,29 @@ const createEdgeLongLabel = (workspace: Workspace, synapses: Record { +export const createNode = ( + nodeId: string, + selected: boolean, + attributes: string[], + position?: Position, + isGroupNode?: boolean, + parent?: string, // Optional parent node ID for compound nodes + activeNeuron?: boolean, +): ElementDefinition => { + let classes = ""; + if (isGroupNode) classes += "groupNode "; + if (selected) classes += "selected "; + if (activeNeuron) classes += "searchedfor "; + const node: ElementDefinition = { group: "nodes", - data: { id: nodeId, label: nodeId, ...attributes.reduce((acc, attr) => ({ ...acc, [attr]: true }), {}) }, - classes: selected ? "selected" : "", + data: { + id: nodeId, + label: nodeId, + ...attributes.reduce((acc, attr) => ({ ...acc, [attr]: true }), {}), + parent: parent || undefined, // Set the parent if provided + }, + classes: classes, }; if (position) { node.position = { x: position.x, y: position.y }; @@ -68,14 +83,26 @@ export const createNode = (nodeId: string, selected: boolean, attributes: string return node; }; -export function applyLayout(cy: Core, layout: string) { - cy.layout({ - name: layout, - }).run(); +export function applyLayout(cy: Core, layout: GRAPH_LAYOUTS) { + const options = getLayoutOptions(cy, layout); + cy.makeLayout(options).run(); refreshLayout(cy); } +function getLayoutOptions(cy: Core, layout: GRAPH_LAYOUTS) { + const baseOptions = LAYOUT_OPTIONS[layout]; + + if (layout === GRAPH_LAYOUTS.Concentric) { + return { + ...baseOptions, + positions: getConcentricLayoutPositions(cy), + }; + } + + return baseOptions; +} + export function refreshLayout(cy: Core) { cy.resize(); // Adjust the viewport size cy.center(); // Center the graph in the container @@ -114,12 +141,12 @@ export const extractNeuronAttributes = (neuron) => { export const getNclassSet = (neuronIds: Set, workspace: Workspace): Set => { const nclassSet = new Set(); - neuronIds.forEach((neuronId) => { + for (const neuronId of neuronIds) { const neuron = workspace.availableNeurons[neuronId]; - if (neuron && neuron.nclass) { + if (neuron?.nclass) { nclassSet.add(neuron.nclass); } - }); + } return nclassSet; }; @@ -128,15 +155,15 @@ export const calculateMeanPosition = (nodeIds: string[], workspace: Workspace): let totalY = 0; let count = 0; - nodeIds.forEach((nodeId) => { - const neuron = workspace.availableNeurons[nodeId]; - const position = neuron?.viewerData[ViewerType.Graph]?.defaultPosition; + for (const nodeId of nodeIds) { + const neuron = workspace.visibilities[nodeId]; + const position = neuron?.[ViewerType.Graph]?.defaultPosition; if (position) { totalX += position.x; totalY += position.y; count++; } - }); + } return { x: totalX / count, @@ -150,27 +177,28 @@ export const calculateSplitPositions = (nodes, basePosition) => { const positions = {}; const n = nodes.length; - if (n == 1) { + if (n === 1) { positions[nodes[0]] = { x: offsetX, y: offsetY }; return positions; - } else if (n == 2) { + } + if (n === 2) { positions[nodes[0]] = { x: offsetX - 35, y: offsetY }; positions[nodes[1]] = { x: offsetX + 35, y: offsetY }; - } else if (n == 3) { + } else if (n === 3) { positions[nodes[0]] = { x: offsetX, y: offsetY - 35 }; positions[nodes[1]] = { x: offsetX - 35, y: offsetY + 35 }; positions[nodes[2]] = { x: offsetX + 35, y: offsetY + 35 }; - } else if (n == 4 && nodes[0] == "RMED") { + } else if (n === 4 && nodes[0] === "RMED") { positions[nodes[0]] = { x: offsetX, y: offsetY - 50 }; positions[nodes[1]] = { x: offsetX - 50, y: offsetY }; positions[nodes[2]] = { x: offsetX + 50, y: offsetY }; positions[nodes[3]] = { x: offsetX, y: offsetY + 50 }; - } else if (n == 4) { + } else if (n === 4) { positions[nodes[0]] = { x: offsetX - 35, y: offsetY - 35 }; positions[nodes[1]] = { x: offsetX + 35, y: offsetY - 35 }; positions[nodes[2]] = { x: offsetX - 35, y: offsetY + 35 }; positions[nodes[3]] = { x: offsetX + 35, y: offsetY + 35 }; - } else if (n == 6) { + } else if (n === 6) { positions[nodes[0]] = { x: offsetX - 35, y: offsetY - 60 }; positions[nodes[1]] = { x: offsetX + 35, y: offsetY - 60 }; positions[nodes[2]] = { x: offsetX - 70, y: offsetY }; @@ -194,12 +222,51 @@ export const updateWorkspaceNeurons2DViewerData = (workspace: Workspace, cy: Cor // Update the workspace availableNeurons with the positions and visibility workspace.customUpdate((draft) => { // Set visibility and position for nodes in the cytoscape graph - cy.nodes().forEach((node) => { + for (const node of cy.nodes()) { const neuronId = node.id(); - if (draft.availableNeurons[neuronId]) { - draft.availableNeurons[neuronId].viewerData[ViewerType.Graph].defaultPosition = { ...node.position() }; - draft.availableNeurons[neuronId].viewerData[ViewerType.Graph].visibility = true; + if (!(neuronId in draft.visibilities)) { + draft.visibilities[neuronId] = emptyViewerData(Visibility.Visible); } - }); + draft.visibilities[neuronId][ViewerType.Graph].defaultPosition = { ...node.position() }; + draft.visibilities[neuronId][ViewerType.Graph].visibility = Visibility.Visible; + } }); }; + +export function getVisibleActiveNeuronsIn2D(workspace: Workspace): Set { + const activeVisibleNeurons = Array.from(workspace.activeNeurons).filter((neuronId) => { + return workspace.visibilities[neuronId]?.[ViewerType.Graph]?.visibility === Visibility.Visible; + }); + + // Create a set to store the class neurons that are active and visible + const activeVisibleClasses = new Set( + activeVisibleNeurons.filter((neuronId) => { + const neuron = workspace.availableNeurons[neuronId]; + return neuron && isNeuronClass(neuronId, workspace); + }), + ); + + // Filter out individual cells if their class neuron is active and visible + return new Set( + activeVisibleNeurons.filter((neuronId) => { + const neuron = workspace.availableNeurons[neuronId]; + const isCellFilteredOut = neuron && neuron.name !== neuron.nclass && activeVisibleClasses.has(neuron.nclass); + + return !isCellFilteredOut; // Return only those that should not be filtered out + }), + ); +} + +export function getHiddenNeuronsIn2D(workspace: Workspace): Set { + return new Set( + Object.entries(workspace.visibilities) + .filter(([_, data]) => data[ViewerType.Graph].visibility === Visibility.Hidden) + .map(([name, _]) => name), + ); +} + +export function isNeuronPartOfClosedGroup(neuronId: string, workspace: Workspace, openGroups: Set): boolean { + return Object.entries(workspace.neuronGroups).some(([groupId, group]) => { + return group.neurons.has(neuronId) && !openGroups.has(groupId); + }); +} diff --git a/applications/visualizer/frontend/src/helpers/workspaceHelper.ts b/applications/visualizer/frontend/src/helpers/workspaceHelper.ts deleted file mode 100644 index d10687d3..00000000 --- a/applications/visualizer/frontend/src/helpers/workspaceHelper.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Dataset } from "../rest"; - -export const getWorkspaceActiveDatasets = (datasets: Record, datasetIds: Set): Record => { - const datasetsArray = Object.values(datasets).filter((dataset) => datasetIds.has(dataset.id)); - return datasetsArray.reduce( - (acc, dataset) => { - acc[dataset.id] = dataset; - return acc; - }, - {} as Record, - ); -}; diff --git a/applications/visualizer/frontend/src/icons/index.tsx b/applications/visualizer/frontend/src/icons/index.tsx index b1162bf7..7fcd56a8 100644 --- a/applications/visualizer/frontend/src/icons/index.tsx +++ b/applications/visualizer/frontend/src/icons/index.tsx @@ -165,3 +165,51 @@ export const GapJunctionIcon = () => ( ); + +export const AlignLeftIcon = () => ( + + + +); + +export const AlignRightIcon = () => ( + + + +); + +export const AlignTopIcon = () => ( + + + +); + +export const AlignBottomIcon = () => ( + + + +); + +export const DistributeHorizontallyIcon = () => ( + + + +); + +export const DistributeVerticallyIcon = () => ( + + + +); diff --git a/applications/visualizer/frontend/src/models/Error.ts b/applications/visualizer/frontend/src/models/Error.ts new file mode 100644 index 00000000..5603609a --- /dev/null +++ b/applications/visualizer/frontend/src/models/Error.ts @@ -0,0 +1,6 @@ +export class GlobalError extends Error { + constructor(message: string) { + super(message); + this.name = "GlobalError"; + } +} diff --git a/applications/visualizer/frontend/src/models/index.ts b/applications/visualizer/frontend/src/models/index.ts index 04e06279..28cc7ff7 100644 --- a/applications/visualizer/frontend/src/models/index.ts +++ b/applications/visualizer/frontend/src/models/index.ts @@ -2,3 +2,5 @@ export { Workspace } from "./workspace.ts"; export type { NeuronGroup } from "./models.ts"; export { ViewMode } from "./models.ts"; export { ViewerType } from "./models.ts"; +export { Alignment } from "./models.ts"; +export { Visibility } from "./models.ts"; diff --git a/applications/visualizer/frontend/src/models/models.ts b/applications/visualizer/frontend/src/models/models.ts index 680d707b..aaa74055 100644 --- a/applications/visualizer/frontend/src/models/models.ts +++ b/applications/visualizer/frontend/src/models/models.ts @@ -26,14 +26,18 @@ export interface NeuronGroup { neurons: Set; } -export interface EnhancedNeuron extends Neuron { - viewerData: ViewerData; - isInteractant: boolean; -} - export interface GraphViewerData { defaultPosition: Position | null; - visibility: boolean; + visibility: Visibility; +} + +export function emptyViewerData(visibility?: Visibility): ViewerData { + return { + [ViewerType.Graph]: { + defaultPosition: null, + visibility: visibility ?? Visibility.Hidden, + }, + }; } export interface ViewerData { @@ -47,18 +51,33 @@ const buildUrlFromFormat = (s: string, param: string) => { return s.replace(s.match("{[^}]+}")?.[0], param); }; -export function getNeuronUrlForDataset(neuron: Neuron, datasetId: string) { - return buildUrlFromFormat(neuron.model3DUrl, datasetId); +export function getNeuronUrlForDataset(neuron: Neuron, datasetId: string): string[] { + return neuron.model3DUrls.map((url) => buildUrlFromFormat(url, datasetId)); } -export function getNeuronURL(dataset: Dataset, neuronName: string) { +export function getNeuronURL(dataset: Dataset, neuronName: string): string { return buildUrlFromFormat(dataset.neuron3DUrl, neuronName); } -export function getSegmentationURL(dataset: Dataset, sliceIndex: number) { +export function getSegmentationURL(dataset: Dataset, sliceIndex: number): string { return buildUrlFromFormat(dataset.emData.segmentation_url, sliceIndex?.toString()); } -export function getEMDataURL(dataset: Dataset, sliceIndex: number) { +export function getEMDataURL(dataset: Dataset, sliceIndex: number): string { return buildUrlFromFormat(dataset.emData.resource_url, sliceIndex?.toString()); } + +export enum Alignment { + Left = "left", + Right = "right", + Top = "top", + Bottom = "bottom", + Horizontal = "Horizontal", + Vertical = "Vertical", +} + +export enum Visibility { + Visible = "Visible", + Hidden = "Hidden", + Unset = "Unset", +} diff --git a/applications/visualizer/frontend/src/models/synchronizer.ts b/applications/visualizer/frontend/src/models/synchronizer.ts new file mode 100644 index 00000000..499dfd66 --- /dev/null +++ b/applications/visualizer/frontend/src/models/synchronizer.ts @@ -0,0 +1,165 @@ +import type { Neuron } from "../rest"; +import { ViewerSynchronizationPair, ViewerType } from "./models"; + +export type SynchronizerContext = Array; +export type Selection = Array; + +const syncViewerDefs: Record = { + [ViewerSynchronizationPair.Graph_InstanceDetails]: [ViewerType.Graph, ViewerType.InstanceDetails], + [ViewerSynchronizationPair.Graph_ThreeD]: [ViewerType.Graph, ViewerType.ThreeD], + [ViewerSynchronizationPair.ThreeD_EM]: [ViewerType.ThreeD, ViewerType.EM], +}; + +class Synchronizer { + active: boolean; + viewers: [ViewerType, ViewerType]; + readonly pair: ViewerSynchronizationPair; + + private constructor(active: boolean, pair: ViewerSynchronizationPair) { + this.active = active; + this.viewers = syncViewerDefs[pair]; + this.pair = pair; + } + + public static create(active: boolean, pair: ViewerSynchronizationPair) { + return new Synchronizer(active, pair); + } + + private canHandle(viewer: ViewerType) { + return this.viewers.includes(viewer); + } + + sync(selection: Array, initiator: ViewerType, contexts: Record) { + if (!this.canHandle(initiator)) { + return; + } + + if (!this.active) { + contexts[initiator] = selection.map((n) => n); + return; + } + + for (const viewer of this.viewers) { + contexts[viewer] = selection.map((n) => n); + } + } + + select(selection: string, initiator: ViewerType, contexts: Record) { + if (!this.canHandle(initiator)) { + return; + } + + if (!this.active) { + contexts[initiator] = [...new Set([...contexts[initiator], selection])]; + return; + } + + for (const viewer of this.viewers) { + contexts[viewer] = [...new Set([...contexts[viewer], selection])]; + } + } + unSelect(selection: string, initiator: ViewerType, contexts: Record) { + if (!this.canHandle(initiator)) { + return; + } + + if (!this.active) { + const storedNodes = [...contexts[initiator]]; + contexts[initiator] = storedNodes.filter((n) => n !== selection); + return; + } + + for (const viewer of this.viewers) { + const storedNodes = [...contexts[viewer]]; + contexts[viewer] = storedNodes.filter((n) => n !== selection); + } + } + + clear(initiator: ViewerType, contexts: Record) { + if (!this.canHandle(initiator)) { + return; + } + + if (!this.active) { + contexts[initiator] = []; + return; + } + + for (const viewer of this.viewers) { + contexts[viewer] = []; + } + } + + setActive(isActive: boolean) { + this.active = isActive; + } +} + +export class SynchronizerOrchestrator { + contexts: Record; + synchronizers: Array; + + private constructor(synchronizers: Array, contexts?: Record) { + this.synchronizers = synchronizers; + if (contexts) { + this.contexts = { ...contexts }; + } else { + this.contexts = { + [ViewerType.EM]: [], + [ViewerType.Graph]: [], + [ViewerType.InstanceDetails]: [], + [ViewerType.ThreeD]: [], + }; + } + } + + public static create(activesSync?: Record, contexts?: Record) { + const synchronizers = [ + Synchronizer.create(activesSync?.[ViewerSynchronizationPair.Graph_InstanceDetails] || true, ViewerSynchronizationPair.Graph_InstanceDetails), + Synchronizer.create(activesSync?.[ViewerSynchronizationPair.Graph_ThreeD] || true, ViewerSynchronizationPair.Graph_ThreeD), + Synchronizer.create(activesSync?.[ViewerSynchronizationPair.ThreeD_EM] || true, ViewerSynchronizationPair.ThreeD_EM), + ]; + + return new SynchronizerOrchestrator(synchronizers, contexts); + } + + public select(selection: Array, initiator: ViewerType) { + for (const synchronizer of this.synchronizers) { + synchronizer.sync(selection, initiator, this.contexts); + } + } + + public selectNeuron(selection: string, initiator: ViewerType) { + for (const synchronizer of this.synchronizers) { + synchronizer.select(selection, initiator, this.contexts); + } + } + + public unSelectNeuron(selection: string, initiator: ViewerType) { + for (const synchronizer of this.synchronizers) { + synchronizer.unSelect(selection, initiator, this.contexts); + } + } + + public clearSelection(initiator: ViewerType) { + for (const synchronizer of this.synchronizers) { + synchronizer.clear(initiator, this.contexts); + } + } + + public getSelection(viewerType: ViewerType): SynchronizerContext { + return this.contexts[viewerType]; + } + public setActive(synchronizer: ViewerSynchronizationPair, isActive: boolean) { + this.synchronizers[synchronizer].setActive(isActive); + } + + public isActive(synchronizer: ViewerSynchronizationPair) { + return this.synchronizers[synchronizer].active; + } + + public switchSynchronizer(syncPair: ViewerSynchronizationPair) { + const synchronizer = this.synchronizers[syncPair]; + synchronizer.setActive(!synchronizer.active); + } +} diff --git a/applications/visualizer/frontend/src/models/workspace.ts b/applications/visualizer/frontend/src/models/workspace.ts index 9123a6eb..fe961ac6 100644 --- a/applications/visualizer/frontend/src/models/workspace.ts +++ b/applications/visualizer/frontend/src/models/workspace.ts @@ -3,7 +3,9 @@ import type { configureStore } from "@reduxjs/toolkit"; import { immerable, produce } from "immer"; import getLayoutManagerAndStore from "../layout-manager/layoutManagerFactory"; import { type Dataset, type Neuron, NeuronsService } from "../rest"; -import { type EnhancedNeuron, type NeuronGroup, ViewerSynchronizationPair, ViewerType } from "./models"; +import { GlobalError } from "./Error.ts"; +import { type NeuronGroup, type ViewerData, type ViewerSynchronizationPair, ViewerType, Visibility, emptyViewerData } from "./models"; +import { type SynchronizerContext, SynchronizerOrchestrator } from "./synchronizer"; export class Workspace { [immerable] = true; @@ -13,78 +15,94 @@ export class Workspace { // datasetID -> Dataset activeDatasets: Record; // neuronID -> Neurons - availableNeurons: Record; + availableNeurons: Record; // neuronId activeNeurons: Set; - selectedNeurons: Set; + visibilities: Record; viewers: Record; - synchronizations: Record; neuronGroups: Record; store: ReturnType; layoutManager: LayoutManager; + + syncOrchestrator: SynchronizerOrchestrator; updateContext: (workspace: Workspace) => void; - constructor(id: string, name: string, activeDatasets: Record, activeNeurons: Set, updateContext: (workspace: Workspace) => void) { + constructor( + id: string, + name: string, + activeDatasets: Record, + activeNeurons: Set, + updateContext: (workspace: Workspace) => void, + activeSynchronizers?: Record, + contexts?: Record, + visibilities?: Record, + ) { this.id = id; this.name = name; this.activeDatasets = activeDatasets; this.availableNeurons = {}; this.activeNeurons = activeNeurons || new Set(); - this.selectedNeurons = new Set(); this.viewers = { - [ViewerType.Graph]: false, - [ViewerType.ThreeD]: true, + [ViewerType.Graph]: true, + [ViewerType.ThreeD]: false, [ViewerType.EM]: false, [ViewerType.InstanceDetails]: false, }; - this.synchronizations = { - [ViewerSynchronizationPair.Graph_InstanceDetails]: true, - [ViewerSynchronizationPair.Graph_ThreeD]: true, - [ViewerSynchronizationPair.ThreeD_EM]: true, - }; this.neuronGroups = {}; const { layoutManager, store } = getLayoutManagerAndStore(id); this.layoutManager = layoutManager; + this.syncOrchestrator = SynchronizerOrchestrator.create(activeSynchronizers, contexts); + + this.visibilities = visibilities || Object.fromEntries([...activeNeurons].map((n) => [n, emptyViewerData(Visibility.Visible)])); + this.store = store; this.updateContext = updateContext; this._initializeAvailableNeurons(); } - activateNeuron(neuron: Neuron): void { + activateNeuron(neuron: Neuron): Workspace { const updated = produce(this, (draft: Workspace) => { draft.activeNeurons.add(neuron.name); - // Set isInteractant to true if the neuron exists in availableNeurons - if (draft.availableNeurons[neuron.name]) { - draft.availableNeurons[neuron.name].isInteractant = true; - } + draft.visibilities[neuron.name] = emptyViewerData(); }); - this.updateContext(updated); + return updated; } deactivateNeuron(neuronId: string): void { const updated = produce(this, (draft: Workspace) => { draft.activeNeurons.delete(neuronId); + delete draft.visibilities[neuronId]; }); this.updateContext(updated); } - deleteNeuron(neuronId: string): void { + hideNeuron(neuronId: string): void { const updated = produce(this, (draft: Workspace) => { - // Remove the neuron from activeNeurons - draft.activeNeurons.delete(neuronId); - - // Set isInteractant to false if the neuron exists in availableNeurons - if (draft.availableNeurons[neuronId]) { - draft.availableNeurons[neuronId].isInteractant = false; + if (!(neuronId in draft.visibilities)) { + draft.visibilities[neuronId] = emptyViewerData(Visibility.Hidden); + draft.removeSelection(neuronId, ViewerType.Graph); } + // todo: add actions for other viewers + draft.visibilities[neuronId][ViewerType.Graph].visibility = Visibility.Hidden; }); this.updateContext(updated); } + showNeuron(neuronId: string): void { + const updated = produce(this, (draft: Workspace) => { + if (!(neuronId in draft.visibilities)) { + draft.visibilities[neuronId] = emptyViewerData(Visibility.Visible); + } + // todo: add actions for other viewers + draft.visibilities[neuronId][ViewerType.Graph].visibility = Visibility.Visible; + }); + + this.updateContext(updated); + } async activateDataset(dataset: Dataset): Promise { const updated: Workspace = produce(this, (draft: Workspace) => { draft.activeDatasets[dataset.id] = dataset; @@ -101,35 +119,22 @@ export class Workspace { const updatedWithNeurons = await this._getAvailableNeurons(updated); this.updateContext(updatedWithNeurons); } - - toggleSelectedNeuron(neuronId: string): void { - const updated = produce(this, (draft: Workspace) => { - if (draft.selectedNeurons.has(neuronId)) { - draft.selectedNeurons.delete(neuronId); - } else { - draft.selectedNeurons.add(neuronId); - } - }); - this.updateContext(updated); - } - setActiveNeurons(newActiveNeurons: Set): void { const updated = produce(this, (draft: Workspace) => { draft.activeNeurons = newActiveNeurons; }); this.updateContext(updated); } - - clearSelectedNeurons(): void { + updateViewerSynchronizationStatus(pair: ViewerSynchronizationPair, isActive: boolean): void { const updated = produce(this, (draft: Workspace) => { - draft.selectedNeurons.clear(); + draft.syncOrchestrator.setActive(pair, isActive); }); this.updateContext(updated); } - updateViewerSynchronizationStatus(pair: ViewerSynchronizationPair, isActive: boolean): void { + switchViewerSynchronizationStatus(pair: ViewerSynchronizationPair): void { const updated = produce(this, (draft: Workspace) => { - draft.synchronizations[pair] = isActive; + draft.syncOrchestrator.switchSynchronizer(pair); }); this.updateContext(updated); } @@ -175,40 +180,27 @@ export class Workspace { const datasetIds = Object.keys(updatedWorkspace.activeDatasets); const neuronArrays = await NeuronsService.searchCells({ datasetIds }); + // Flatten and add neurons classes const uniqueNeurons = new Set(); - - // Flatten and deduplicate neurons - for (const neuronArray of neuronArrays.flat()) { - uniqueNeurons.add(neuronArray); - const classNeuron = { ...neuronArray, name: neuronArray.nclass }; - uniqueNeurons.add(classNeuron); + const neuronsClass: Record = {}; + for (const neuron of neuronArrays.flat()) { + uniqueNeurons.add(neuron); + + const className = neuron.nclass; + if (!(className in neuronsClass)) { + const neuronClass = { ...neuron, name: className }; + neuronsClass[className] = neuronClass; + uniqueNeurons.add(neuronClass); + } else { + neuronsClass[className].model3DUrls.push(...neuron.model3DUrls); + } } return produce(updatedWorkspace, (draft: Workspace) => { - draft.availableNeurons = {}; - for (const neuron of uniqueNeurons) { - const previousNeuron = draft.availableNeurons[neuron.name]; - - const enhancedNeuron: EnhancedNeuron = { - ...neuron, - viewerData: { - [ViewerType.Graph]: { - defaultPosition: previousNeuron?.viewerData[ViewerType.Graph]?.defaultPosition || null, - visibility: previousNeuron?.viewerData[ViewerType.Graph]?.visibility || false, - }, - [ViewerType.ThreeD]: previousNeuron?.viewerData[ViewerType.ThreeD] || {}, - [ViewerType.EM]: previousNeuron?.viewerData[ViewerType.EM] || {}, - [ViewerType.InstanceDetails]: previousNeuron?.viewerData[ViewerType.InstanceDetails] || {}, - }, - isInteractant: previousNeuron?.isInteractant ?? draft.activeNeurons.has(neuron.name), - }; - - draft.availableNeurons[neuron.name] = enhancedNeuron; - } + draft.availableNeurons = Object.fromEntries([...uniqueNeurons].map((n) => [n.name, n])); }); } catch (error) { - console.error("Failed to fetch neurons:", error); - return updatedWorkspace; + throw new GlobalError("Failed to fetch neurons:"); } } @@ -216,4 +208,48 @@ export class Workspace { const updated = produce(this, updateFunction); this.updateContext(updated); } + + setSelection(selection: Array, initiator: ViewerType) { + this.customUpdate((draft) => { + draft.syncOrchestrator.select(selection, initiator); + }); + } + clearSelection(initiator: ViewerType): Workspace { + const updated = produce(this, (draft: Workspace) => { + draft.syncOrchestrator.clearSelection(initiator); + }); + this.updateContext(updated); + return updated; + } + + addSelection(selection: string, initiator: ViewerType) { + this.customUpdate((draft) => { + draft.syncOrchestrator.selectNeuron(selection, initiator); + }); + } + + removeSelection(selection: string, initiator: ViewerType) { + this.customUpdate((draft) => { + draft.syncOrchestrator.unSelectNeuron(selection, initiator); + }); + } + + getSelection(viewerType: ViewerType): string[] { + return this.syncOrchestrator.getSelection(viewerType); + } + + getViewerSelecedNeurons(viewerType: ViewerType): string[] { + return this.syncOrchestrator.getSelection(viewerType); + } + + getNeuronCellsByClass(neuronClassId: string): string[] { + return Object.values(this.availableNeurons) + .filter((neuron) => neuron.nclass === neuronClassId && neuron.nclass !== neuron.name) + .map((neuron) => neuron.name); + } + + getNeuronClass(neuronId: string): string { + const neuron = this.availableNeurons[neuronId]; + return neuron.nclass; + } } diff --git a/applications/visualizer/frontend/src/rest/models/Neuron.ts b/applications/visualizer/frontend/src/rest/models/Neuron.ts index 7fcaa049..5e371bb3 100644 --- a/applications/visualizer/frontend/src/rest/models/Neuron.ts +++ b/applications/visualizer/frontend/src/rest/models/Neuron.ts @@ -3,10 +3,9 @@ /* tslint:disable */ /* eslint-disable */ export type Neuron = { - id: string; name: string; datasetIds: Array; - model3DUrl: string; + model3DUrls: Array; nclass: string; neurotransmitter: string; type: string; diff --git a/applications/visualizer/frontend/src/settings/templateWorkspaceSettings.ts b/applications/visualizer/frontend/src/settings/templateWorkspaceSettings.ts index 2afe725a..962a4cb6 100644 --- a/applications/visualizer/frontend/src/settings/templateWorkspaceSettings.ts +++ b/applications/visualizer/frontend/src/settings/templateWorkspaceSettings.ts @@ -1,2 +1,2 @@ export const TEMPLATE_ACTIVE_DATASETS = ["witvliet_2020_7", "witvliet_2020_8"]; -export const TEMPLATE_ACTIVE_NEURONS = ["ASEL", "AIYR"]; +export const TEMPLATE_ACTIVE_NEURONS = ["AIY", "ASEL"]; diff --git a/applications/visualizer/frontend/src/settings/twoDSettings.tsx b/applications/visualizer/frontend/src/settings/twoDSettings.tsx index 631e970a..d5c5e1b1 100644 --- a/applications/visualizer/frontend/src/settings/twoDSettings.tsx +++ b/applications/visualizer/frontend/src/settings/twoDSettings.tsx @@ -1,5 +1,6 @@ -import { ChemicalSynapseIcon, GapJunctionIcon } from "../icons"; import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; +import type { LayoutOptions, NodeCollection, NodeSingular } from "cytoscape"; +import { ChemicalSynapseIcon, GapJunctionIcon } from "../icons"; export const CHEMICAL_THRESHOLD = 3; export const ELECTRICAL_THRESHOLD = 2; @@ -16,6 +17,52 @@ export enum GRAPH_LAYOUTS { Hierarchical = "dagre", Concentric = "concentric", } +type FcoseLayoutOptions = LayoutOptions & { + nodeRepulsion: number; + idealEdgeLength: number; + numIter: number; + nestingFactor: number; + gravity: number; +}; + +type DagreLayoutOptions = LayoutOptions & { + rankDir: string; + nodeSep: number; + rankSep: number; +}; + +type ConcentricLayoutOptions = LayoutOptions & { + concentric: (node: NodeSingular) => number; + levelWidth: (nodes: NodeCollection) => number; + spacingFactor: number; +}; + +type ExtendedLayoutOptions = FcoseLayoutOptions | DagreLayoutOptions | ConcentricLayoutOptions; + +export const LAYOUT_OPTIONS: Record = { + [GRAPH_LAYOUTS.Force]: { + name: "fcose", + nodeRepulsion: 1500, + idealEdgeLength: 150, + numIter: 2500, + nestingFactor: 0.1, + gravity: 0.2, + }, + [GRAPH_LAYOUTS.Hierarchical]: { + name: "dagre", + rankDir: "TB", + nodeSep: 10, + rankSep: 100, + }, + [GRAPH_LAYOUTS.Concentric]: { + name: "preset", + concentric: (node: NodeSingular) => node.degree(true), + levelWidth: (nodes: NodeCollection) => nodes.maxDegree(true) / 4, + animate: false, + padding: 30, + spacingFactor: 1, + }, +}; export enum LegendType { Node = 0, @@ -78,3 +125,9 @@ export const annotationLegend = { icon: , }, }; + +export const SELECTED_CLASS = "selected"; +export const FADED_CLASS = "faded"; +export const HOVER_CLASS = "hover"; +export const FOCUS_CLASS = "focus"; +export const SHOW_EDGE_LABEL_CLASS = "showEdgeLabel"; diff --git a/applications/visualizer/frontend/src/theme/index.tsx b/applications/visualizer/frontend/src/theme/index.tsx index ad7567c8..81a9ecce 100644 --- a/applications/visualizer/frontend/src/theme/index.tsx +++ b/applications/visualizer/frontend/src/theme/index.tsx @@ -1,4 +1,4 @@ -import { createTheme } from "@mui/material/styles"; +import createTheme from "@mui/material/styles/createTheme"; import { vars } from "./variables.ts"; const { primaryFont, diff --git a/applications/visualizer/frontend/src/theme/twoDStyles.ts b/applications/visualizer/frontend/src/theme/twoDStyles.ts index 545e4e21..337bc2d2 100644 --- a/applications/visualizer/frontend/src/theme/twoDStyles.ts +++ b/applications/visualizer/frontend/src/theme/twoDStyles.ts @@ -1,16 +1,11 @@ import type { Stylesheet } from "cytoscape"; import { annotationLegend } from "../settings/twoDSettings.tsx"; -const NODE_STYLE = { - "background-color": "#666", - label: "data(label)", - color: "#fff", - "text-valign": "center", - "text-halign": "center", - "font-size": 8, - width: 24, - height: 24, -}; +const searchedforNeuronBackground = + "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NSIgaGVpZ2h0PSI2NSIgdmlld0JveD0iMCAwIDY1IDY1Ij48ZWxsaXBzZSByeT0iMTkuODkiIHJ4PSIuOTc1IiBjeT0iLS4wMTkiIGN4PSIyNC45NDgiIHRyYW5zZm9ybT0icm90YXRlKDQ1KSIvPjxlbGxpcHNlIHJ5PSIyOS4wNTUiIHJ4PSIuOTc1IiBjeT0iLjA0NiIgY3g9IjQ2LjAwOCIgdHJhbnNmb3JtPSJyb3RhdGUoNDUpIi8+PGVsbGlwc2Ugcnk9IjEyLjE1NSIgcng9Ii45NzUiIGN5PSItLjAxOSIgY3g9IjcyLjMzMyIgdHJhbnNmb3JtPSJyb3RhdGUoNDUpIi8+PGVsbGlwc2Ugcnk9IjEyLjE1NSIgcng9Ii45NzUiIGN5PSItLjAxOSIgY3g9IjE5LjY4MyIgdHJhbnNmb3JtPSJyb3RhdGUoNDUpIi8+PGVsbGlwc2Ugcnk9IjI0LjIxMiIgcng9Ii45NzUiIGN5PSItLjAxOSIgY3g9IjYxLjgwMyIgdHJhbnNmb3JtPSJyb3RhdGUoNDUpIi8+PGVsbGlwc2Ugcnk9IjI2LjkxIiByeD0iLjk3NSIgY3k9Ii0uMDE5IiBjeD0iNTYuNTM4IiB0cmFuc2Zvcm09InJvdGF0ZSg0NSkiLz48ZWxsaXBzZSByeT0iMjguNTY3IiByeD0iLjk3NSIgY3k9Ii0uMDE5IiBjeD0iNTEuMjczIiB0cmFuc2Zvcm09InJvdGF0ZSg0NSkiLz48ZWxsaXBzZSByeT0iMjQuMjEyIiByeD0iLjk3NSIgY3k9Ii0uMDE5IiBjeD0iMzAuMjEzIiB0cmFuc2Zvcm09InJvdGF0ZSg0NSkiLz48ZWxsaXBzZSByeT0iMjYuOTEiIHJ4PSIuOTc1IiBjeT0iLS4wMTkiIGN4PSIzNS40NzgiIHRyYW5zZm9ybT0icm90YXRlKDQ1KSIvPjxlbGxpcHNlIHJ5PSIyOC41NjciIHJ4PSIuOTc1IiBjeT0iLS4wMTkiIGN4PSI0MC43NDMiIHRyYW5zZm9ybT0icm90YXRlKDQ1KSIvPjxlbGxpcHNlIHJ5PSIxOS44OSIgcng9Ii45NzUiIGN5PSItLjAxOSIgY3g9IjY3LjA2OCIgdHJhbnNmb3JtPSJyb3RhdGUoNDUpIi8+PC9zdmc+"; // original: image/node_background_neuron.svg + +const searchedforMuscleBackground = + "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyOSIgaGVpZ2h0PSIyOSIgdmlld0JveD0iMCAwIDI5IDI5Ij48ZyB0cmFuc2Zvcm09InJvdGF0ZSg0NSAxMjM1LjMwOCAtNTExLjY4KSI+PHBhdGggZD0iTTcyOC4yNCA3MTkuMDE4YS45NzcgMjAuNDgyIDAgMCAwLS43MjQgOC41bDEuNjM3IDEuNjM2YS45NzcgMjAuNDgyIDAgMCAwLS44NDQtMTAuMTg2Ljk3NyAyMC40ODIgMCAwIDAtLjA3LjA1ek03NDguNTgyIDcwNy41N2EuOTc3IDIwLjQ4MiAwIDAgMC0uMTQ3IDEwLjc3Ni45NzcgMjAuNDgyIDAgMCAwIC45NzcgMjAuNDgyLjk3NyAyMC40ODIgMCAwIDAgLjk3Ny0yMC40OC45NzcgMjAuNDgyIDAgMCAwLS4xMDMtOS4wNzJsLTEuNzA1LTEuNzA2ek03NTkuMTIgNzE4LjExYS45NzcgMjAuNDgyIDAgMCAwIC44NDMgMTAuMTY3Ljk3NyAyMC40ODIgMCAwIDAgLjc5My04LjUzMmwtMS42MzYtMS42MzZ6TTc1My43MTIgNzEyLjdhLjk3NyAyMC40ODIgMCAwIDAgMCAuMzcuOTc3IDIwLjQ4MiAwIDAgMCAuOTc1IDIwLjQ4My45NzcgMjAuNDgyIDAgMCAwIC45NzQtMTguOTAzbC0xLjk0OC0xLjk1ek03MzMuNTE1IDcxMy43NDJhLjk3NyAyMC40ODIgMCAwIDAtLjkwNCAxOC44N2wxLjk1IDEuOTVhLjk3NyAyMC40ODIgMCAwIDAgLjAwMi0uMzg4Ljk3NyAyMC40ODIgMCAwIDAtLjk3Ny0yMC40OC45NzcgMjAuNDgyIDAgMCAwLS4wNy4wNDh6TTczOC43OSA3MDguNDY3YS45NzcgMjAuNDgyIDAgMCAwLS45MDggMjAuNDMuOTc3IDIwLjQ4MiAwIDAgMCAuMTAzIDkuMDlsMS43MDQgMS43MDNhLjk3NyAyMC40ODIgMCAwIDAgLjE0OC0xMC43OTIuOTc3IDIwLjQ4MiAwIDAgMC0uOTc2LTIwLjQ4Mi45NzcgMjAuNDgyIDAgMCAwLS4wNy4wNXoiLz48ZWxsaXBzZSByeT0iMjAuNDgyIiByeD0iLjk3NyIgY3k9IjcyMy42MjIiIGN4PSI3NDQuMTM2Ii8+PC9nPjwvc3ZnPg=="; // original: image/node_background_muscle.svg const SELECTED_NODE_STYLE = { "border-width": 2, @@ -18,16 +13,143 @@ const SELECTED_NODE_STYLE = { "border-opacity": 1, }; -const EDGE_STYLE = { - "line-color": "#63625F", - "target-arrow-color": "#63625F", - "target-arrow-shape": "triangle", - "curve-style": "bezier", - "arrow-scale": 0.3, +const SEARCHED_FOR_NODE_STYLE = { + "background-image": searchedforNeuronBackground, + "background-image-opacity": ".2", + "z-index": 0, + "background-repeat": "no-repeat", + "background-height": 22, + "background-width": 22, +}; + +const GROUP_NODE_STYLE = { + label: "Group", + shape: "roundrectangle", + "background-color": "#ECECE9", + "font-size": "10px", + width: 20, + height: 20, + "background-opacity": 0.7, + padding: "7px", + "text-wrap": "wrap", + "text-valign": "center", + "text-halign": "center", + color: "black", + "border-width": 1, + "border-color": "black", + "font-weight": "normal", + "background-image": "none", + "pie-size": "75%", + "border-opacity": 1, +}; + +const UNSELECTED_GROUP_NODE_STYLE = { + ...GROUP_NODE_STYLE, + "background-color": "#D3D3CF", + "border-width": 0, }; -const CHEMICAL_STYLE = { "line-color": "#63625F", width: 0.5 }; -const ELECTRICAL_STYLE = { "line-color": "yellow", width: 0.5 }; +const EDGE_STYLE = [ + { + selector: "edge", + style: { + "line-color": "#63625F", + "target-arrow-color": "#63625F", + "target-arrow-shape": "triangle", + "curve-style": "bezier", + "arrow-scale": 0.6, + "source-distance-from-node": 1, + "target-distance-from-node": 1, + width: (node: any) => node.data("width"), + }, + }, + { + selector: "edge:loop", + css: { + "source-distance-from-node": 0, + "target-distance-from-node": 0, + "arrow-scale": 0.6, + }, + }, +]; + +const CHEMICAL_STYLE = [ + { + selector: ".chemical", + style: { "line-color": "#63625F", width: (node: any) => node.data("width") }, + }, + { + selector: "edge.chemical.parallel", + style: { + "curve-style": "unbundled-bezier", + "control-point-distances": 40, + "control-point-weights": 0.5, + }, + }, +]; + +const ELECTRICAL_STYLE = [ + { + selector: ".electrical", + style: { + "line-color": "#63625F", + width: (node: any) => node.data("width"), + "curve-style": "segments", + "target-arrow-color": "#666666", + "source-arrow-color": "#666666", + "segment-distances": "0 -4 4 -4 4 0", + "segment-weights": (ele) => { + const sourcePos = ele.source().position(); + const targetPos = ele.target().position(); + const length = Math.sqrt(Math.pow(targetPos.x - sourcePos.x, 2) + Math.pow(targetPos.y - sourcePos.y, 2)); + const divider = (length > 60 ? 7 : length > 40 ? 5 : 3) / length; + return [-2.0, -1.5, -0.5, 0.5, 1.5, 2.0].map((d) => 0.5 + d * divider).join(" "); + }, + "target-arrow-shape": "none", + }, + }, + { + selector: ".electrical:loop", + css: { + "target-arrow-shape": "tee", + "source-arrow-shape": "tee", + }, + }, + { + selector: ".electrical.not_classified:loop", + css: { + "target-arrow-shape": "tee", + "source-arrow-shape": "tee", + "source-arrow-color": "#228B22", + "target-arrow-color": "#228B22", + }, + }, +]; + +const OPEN_GROUP_STYLE = { + "background-image": "none", + "pie-size": "0%", + "text-valign": "top", + "text-halign": "center", + "text-background-opacity": 1, + "text-background-shape": "roundrectangle", + "text-border-width": 2, + "text-background-padding": "2px", + "text-margin-y": "-3px", + "text-border-opacity": 1, + "background-color": "#eaeaea", + "border-color": "#d0d0d0", + "text-background-color": "#d0d0d0", + "text-border-color": "#d0d0d0", + "border-opacity": 1, + shape: "roundrectangle", + "font-size": "10px", + "background-opacity": 0.7, + "text-wrap": "wrap", + color: "black", + "border-width": 3, + "font-weight": "bold", +}; const FADED_STYLE = [ { @@ -50,7 +172,15 @@ const EDGE_LABEL_STYLES = [ selector: "edge.hover, edge.showEdgeLabel", style: { label: "data(label)", - "font-size": "8px", + "font-size": "12px", + "font-weight": "bold", + "text-background-opacity": 0, + "text-background-padding": "3px", + "z-index": 10, + "z-compound-depth": "top", + shape: "roundrectangle", + "text-outline-width": 0.8, + "text-outline-color": "rgb(244,244,244)", }, }, { @@ -59,39 +189,90 @@ const EDGE_LABEL_STYLES = [ label: "data(longLabel)", "font-size": "8px", "text-wrap": "wrap", + "text-background-opacity": 0, + "text-background-padding": "3px", + "z-index": 10, }, }, ]; -const ANNOTATION_STYLES = Object.entries(annotationLegend).map(([, { id, color }]) => ({ - selector: `.${id}`, - style: { - "line-color": color, - "target-arrow-color": color, - }, -})); - -export const GRAPH_STYLES = [ +const NODE_STYLE = [ { selector: "node", - style: NODE_STYLE, + style: { + "background-color": "#666", + label: "data(label)", + color: "black", + "text-valign": "center", + "text-halign": "center", + "font-size": 8, + width: 24, + height: 24, + }, + }, + { + selector: "node.groupNode.selected", + style: GROUP_NODE_STYLE, + }, + { + selector: "node.groupNode", + style: UNSELECTED_GROUP_NODE_STYLE, }, { selector: "node.selected", style: SELECTED_NODE_STYLE, }, { - selector: "edge", - style: EDGE_STYLE, + selector: "node.searchedfor", + style: SEARCHED_FOR_NODE_STYLE, }, { - selector: ".chemical", - style: CHEMICAL_STYLE, + selector: ":parent", + style: OPEN_GROUP_STYLE, }, { - selector: ".electrical", - style: ELECTRICAL_STYLE, + selector: ":parent.selected", + style: { + ...OPEN_GROUP_STYLE, + "font-weight": "bold", + padding: "9px", + "text-margin-y": "-4px", + }, + }, + { + selector: "node[?none], node[?muscle], node[?others]", + css: { + "font-size": 8, + shape: "roundrectangle", + height: "10px", + padding: "8px", + "z-index": 10, + }, + }, + { + selector: "node.searchedfor[?none], node.searchedfor[?muscle], node.searchedfor[?others]", + css: { + "background-image": searchedforMuscleBackground, + "background-repeat": "repeat-x", + "background-image-opacity": "0.1", + }, }, +]; + +const ANNOTATION_STYLES = Object.entries(annotationLegend).map(([, { id, color }]) => ({ + selector: `.${id}`, + style: { + "line-color": color, + "target-arrow-color": color, + "source-arrow-color": color, + }, +})); + +export const GRAPH_STYLES = [ + ...NODE_STYLE, + ...EDGE_STYLE, + ...ELECTRICAL_STYLE, + ...CHEMICAL_STYLE, ...EDGE_LABEL_STYLES, ...FADED_STYLE, ...ANNOTATION_STYLES, diff --git a/applications/visualizer/frontend/yarn.lock b/applications/visualizer/frontend/yarn.lock index ea6c8545..ef40a121 100644 --- a/applications/visualizer/frontend/yarn.lock +++ b/applications/visualizer/frontend/yarn.lock @@ -881,6 +881,11 @@ redux-thunk "^3.1.0" reselect "^5.1.0" +"@remix-run/router@1.19.2": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.19.2.tgz#0c896535473291cb41f152c180bedd5680a3b273" + integrity sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA== + "@rollup/rollup-android-arm-eabi@4.19.0": version "4.19.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.0.tgz#3d9fd50164b94964f5de68c3c4ce61933b3a338d" @@ -1177,6 +1182,11 @@ babel-plugin-macros@^3.1.0: cosmiconfig "^7.0.0" resolve "^1.19.0" +base64-arraybuffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" + integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== + base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -1364,6 +1374,13 @@ cross-spawn@^7.0.1: shebang-command "^2.0.0" which "^2.0.1" +css-line-break@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0" + integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w== + dependencies: + utrie "^1.0.2" + css-vendor@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d" @@ -1632,6 +1649,14 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react- dependencies: react-is "^16.7.0" +html2canvas@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543" + integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA== + dependencies: + css-line-break "^2.1.0" + text-segmentation "^1.0.3" + hyphenate-style-name@^1.0.3: version "1.1.0" resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz#1797bf50369588b47b72ca6d5e65374607cf4436" @@ -1958,7 +1983,7 @@ pako@^1.0.3: resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== -pako@^2.0.4: +pako@^2.0.4, pako@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== @@ -2173,6 +2198,21 @@ react-rnd@^7.3.0: re-resizable "4.5.1" react-draggable "^3.0.5" +react-router-dom@^6.26.2: + version "6.26.2" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.26.2.tgz#a6e3b0cbd6bfd508e42b9342099d015a0ac59680" + integrity sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ== + dependencies: + "@remix-run/router" "1.19.2" + react-router "6.26.2" + +react-router@6.26.2: + version "6.26.2" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.26.2.tgz#2f0a68999168954431cdc29dd36cec3b6fa44a7e" + integrity sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A== + dependencies: + "@remix-run/router" "1.19.2" + react-transition-group@^4.4.0, react-transition-group@^4.4.5: version "4.4.5" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" @@ -2386,6 +2426,13 @@ suspend-react@^0.1.3: resolved "https://registry.yarnpkg.com/suspend-react/-/suspend-react-0.1.3.tgz#a52f49d21cfae9a2fb70bd0c68413d3f9d90768e" integrity sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ== +text-segmentation@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943" + integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw== + dependencies: + utrie "^1.0.2" + three-mesh-bvh@^0.7.0: version "0.7.6" resolved "https://registry.yarnpkg.com/three-mesh-bvh/-/three-mesh-bvh-0.7.6.tgz#6764b39475bdba9450ad3a4065492678ee537272" @@ -2500,6 +2547,13 @@ utility-types@^3.10.0: resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.11.0.tgz#607c40edb4f258915e901ea7995607fdf319424c" integrity sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw== +utrie@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645" + integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw== + dependencies: + base64-arraybuffer "^1.0.2" + uuid@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"