From 0719d2f761bae0a287f4010583224a6ba9f1b077 Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Thu, 19 Dec 2024 15:20:09 +0100 Subject: [PATCH 1/7] feat: support for multi-picking and cursor cloning Minor bug fixes in python folder, syncing of React versions --- python/package-lock.json | 390 +++++++++--------- python/package.json | 9 +- .../SubsurfaceViewer/SubsurfaceViewer.tsx | 202 ++++++++- .../WellLogViewer/WellLogViewer.tsx | 9 +- .../subsurface-viewer/src/components/View.tsx | 29 ++ .../hooks/useMultiViewCursorTracking/index.ts | 6 + .../useMultiViewCursorTracking.ts | 62 +++ .../MultiViewPickingInfoAssembler.ts | 208 ++++++++++ .../src/hooks/useMultiViewPicking/index.ts | 3 + .../useMultiViewPicking.ts | 79 ++++ .../src/layers/crosshair/crosshairLayer.ts | 68 +++ .../subsurface-viewer/src/layers/index.ts | 2 + .../examples/MultiViewExamples.stories.tsx | 280 ++++--------- 13 files changed, 931 insertions(+), 416 deletions(-) create mode 100644 typescript/packages/subsurface-viewer/src/components/View.tsx create mode 100644 typescript/packages/subsurface-viewer/src/hooks/useMultiViewCursorTracking/index.ts create mode 100644 typescript/packages/subsurface-viewer/src/hooks/useMultiViewCursorTracking/useMultiViewCursorTracking.ts create mode 100644 typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/MultiViewPickingInfoAssembler.ts create mode 100644 typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/index.ts create mode 100644 typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/useMultiViewPicking.ts create mode 100644 typescript/packages/subsurface-viewer/src/layers/crosshair/crosshairLayer.ts diff --git a/python/package-lock.json b/python/package-lock.json index dedab73c8d..6720272fcc 100644 --- a/python/package-lock.json +++ b/python/package-lock.json @@ -10,7 +10,8 @@ "hasInstallScript": true, "license": "MPL", "dependencies": { - "@deck.gl/core": "^8.9.35", + "@deck.gl/core": "^9.0.36", + "@deck.gl/react": "^9.0.36", "@emerson-eps/color-tables": "^0.4.85", "@equinor/eds-core-react": "0.33.0", "@equinor/eds-icons": "^0.19.1", @@ -27,8 +28,8 @@ "leaflet-draw": "^1.0.4", "lodash": "^4.17.21", "mathjs": "^9.4.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-dropdown-tree-select": "^2.8.0", "react-redux": "^8.1.1", "react-resize-detector": "^9.0.0" @@ -43,7 +44,7 @@ "@types/leaflet": "^1.8.0", "@types/leaflet-draw": "^1.0.8", "@types/lodash": "^4.14.199", - "@types/react": "^18.2.7", + "@types/react": "^18.3.12", "@types/react-dom": "^18.2.7", "@typescript-eslint/eslint-plugin": "^6.4.0", "@typescript-eslint/parser": "^6.4.0", @@ -74,7 +75,7 @@ }, "../typescript/packages/group-tree-plot": { "name": "@webviz/group-tree-plot", - "version": "1.3.15", + "version": "1.3.20", "license": "MPL-2.0", "dependencies": { "d3": "^7.8.2", @@ -87,18 +88,18 @@ }, "../typescript/packages/subsurface-viewer": { "name": "@webviz/subsurface-viewer", - "version": "1.1.1", + "version": "1.3.1", "license": "MPL-2.0", "dependencies": { "@deck.gl-community/editable-layers": "^9.0.3", - "@deck.gl/aggregation-layers": "^9.0.33", - "@deck.gl/core": "^9.0.33", - "@deck.gl/extensions": "^9.0.33", - "@deck.gl/geo-layers": "^9.0.33", - "@deck.gl/json": "^9.0.33", - "@deck.gl/layers": "^9.0.33", - "@deck.gl/mesh-layers": "^9.0.33", - "@deck.gl/react": "^9.0.33", + "@deck.gl/aggregation-layers": "^9.0.36", + "@deck.gl/core": "^9.0.36", + "@deck.gl/extensions": "^9.0.36", + "@deck.gl/geo-layers": "^9.0.36", + "@deck.gl/json": "^9.0.36", + "@deck.gl/layers": "^9.0.36", + "@deck.gl/mesh-layers": "^9.0.36", + "@deck.gl/react": "^9.0.36", "@emerson-eps/color-tables": "^0.4.85", "@equinor/eds-core-react": "^0.36.0", "@equinor/eds-icons": "^0.21.0", @@ -115,10 +116,10 @@ "math.gl": "^4.0.1", "mathjs": "^13.2.0", "merge-refs": "^1.2.2", - "workerpool": "^9.1.3" + "workerpool": "^9.2.0" }, "devDependencies": { - "@reduxjs/toolkit": "^2.2.8", + "@reduxjs/toolkit": "^2.3.0", "react-redux": "^9.1.2" }, "peerDependencies": { @@ -143,7 +144,7 @@ }, "../typescript/packages/well-completions-plot": { "name": "@webviz/well-completions-plot", - "version": "1.5.11", + "version": "1.5.16", "license": "MPL-2.0", "dependencies": { "react-resize-detector": "^11.0.1", @@ -155,7 +156,7 @@ }, "../typescript/packages/well-log-viewer": { "name": "@webviz/well-log-viewer", - "version": "2.1.6", + "version": "2.2.1", "license": "MPL-2.0", "dependencies": { "@emerson-eps/color-tables": "^0.4.85", @@ -165,7 +166,7 @@ "d3": "^7.8.2" }, "devDependencies": { - "@reduxjs/toolkit": "^2.2.8", + "@reduxjs/toolkit": "^2.3.0", "react-redux": "^9.1.2" }, "peerDependencies": { @@ -179,10 +180,10 @@ }, "../typescript/packages/wsc-common": { "name": "@webviz/wsc-common", - "version": "1.0.6", + "version": "1.0.11", "license": "MPL-2.0", "dependencies": { - "@deck.gl/core": "^9.0.33", + "@deck.gl/core": "^9.0.36", "ajv": "^8.12.0" } }, @@ -557,27 +558,38 @@ "license": "MIT" }, "node_modules/@deck.gl/core": { - "version": "8.9.35", - "resolved": "https://registry.npmjs.org/@deck.gl/core/-/core-8.9.35.tgz", - "integrity": "sha512-xOASWScUCB5fpfuSjPaJrwas8pCJpbKXNIfwQElhvnfP3Yk8GGkAcRbPgiPNCfpkbEno7eDpAWJt6+6UJsSp9g==", - "dependencies": { - "@babel/runtime": "^7.0.0", - "@loaders.gl/core": "^3.4.13", - "@loaders.gl/images": "^3.4.13", - "@luma.gl/constants": "^8.5.21", - "@luma.gl/core": "^8.5.21", - "@luma.gl/webgl": "^8.5.21", - "@math.gl/core": "^3.6.2", - "@math.gl/sun": "^3.6.2", - "@math.gl/web-mercator": "^3.6.2", - "@probe.gl/env": "^3.5.0", - "@probe.gl/log": "^3.5.0", - "@probe.gl/stats": "^3.5.0", + "version": "9.0.36", + "resolved": "https://registry.npmjs.org/@deck.gl/core/-/core-9.0.36.tgz", + "integrity": "sha512-Muhty9C5smnITbx7XoiiG465DI+qpNT7y2yfQPPdBYrUkKLYTD9qJRMHZcLcwSuEJj6JmopmkcqOpraxiOc0Ew==", + "dependencies": { + "@loaders.gl/core": "^4.2.0", + "@loaders.gl/images": "^4.2.0", + "@luma.gl/constants": "~9.0.27", + "@luma.gl/core": "~9.0.27", + "@luma.gl/engine": "~9.0.27", + "@luma.gl/shadertools": "~9.0.27", + "@luma.gl/webgl": "~9.0.27", + "@math.gl/core": "^4.0.0", + "@math.gl/sun": "^4.0.0", + "@math.gl/web-mercator": "^4.0.0", + "@probe.gl/env": "^4.0.9", + "@probe.gl/log": "^4.0.9", + "@probe.gl/stats": "^4.0.9", + "@types/offscreencanvas": "^2019.6.4", "gl-matrix": "^3.0.0", - "math.gl": "^3.6.2", "mjolnir.js": "^2.7.0" } }, + "node_modules/@deck.gl/react": { + "version": "9.0.36", + "resolved": "https://registry.npmjs.org/@deck.gl/react/-/react-9.0.36.tgz", + "integrity": "sha512-x7EaxkUCT2N9x6rPyL+Lchh3CvitZLZI8G4QvXvlwglvdamUCI42L7+91HecpI5DDYkxuZluql2c9TWGoC7TXQ==", + "peerDependencies": { + "@deck.gl/core": "^9.0.0", + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "dev": true, @@ -1174,132 +1186,124 @@ "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" }, "node_modules/@loaders.gl/core": { - "version": "3.4.14", - "resolved": "https://registry.npmjs.org/@loaders.gl/core/-/core-3.4.14.tgz", - "integrity": "sha512-5PFcjv7xC8AYL17juDMrvo8n0Fcwg9s8F4BaM2YCNUsb9RCI2SmLuIFJMcx1GgHO5vL0WiTIKO+JT4n1FuNR6w==", - "dependencies": { - "@babel/runtime": "^7.3.1", - "@loaders.gl/loader-utils": "3.4.14", - "@loaders.gl/worker-utils": "3.4.14", - "@probe.gl/log": "^4.0.1" - } - }, - "node_modules/@loaders.gl/core/node_modules/@probe.gl/env": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@probe.gl/env/-/env-4.0.4.tgz", - "integrity": "sha512-sYNGqesDfWD6dFP5oNZtTeFA4Z6ak5T4a8BNPdNhoqy7PK9w70JHrb6mv+RKWqKXq33KiwCDWL7fYxx2HuEH2w==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@loaders.gl/core/-/core-4.3.3.tgz", + "integrity": "sha512-RaQ3uNg4ZaVqDRgvJ2CjaOjeeHdKvbKuzFFgbGnflVB9is5bu+h3EKc3Jke7NGVvLBsZ6oIXzkwHijVsMfxv8g==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.0.0" - } - }, - "node_modules/@loaders.gl/core/node_modules/@probe.gl/log": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@probe.gl/log/-/log-4.0.4.tgz", - "integrity": "sha512-WpmXl6njlBMwrm8HBh/b4kSp/xnY1VVmeT4PWUKF+RkVbFuKQbsU11dA1IxoMd7gSY+5DGIwxGfAv1H5OMzA4A==", - "dependencies": { - "@babel/runtime": "^7.0.0", - "@probe.gl/env": "4.0.4" + "@loaders.gl/loader-utils": "4.3.3", + "@loaders.gl/schema": "4.3.3", + "@loaders.gl/worker-utils": "4.3.3", + "@probe.gl/log": "^4.0.2" } }, "node_modules/@loaders.gl/images": { - "version": "3.4.14", - "resolved": "https://registry.npmjs.org/@loaders.gl/images/-/images-3.4.14.tgz", - "integrity": "sha512-tL447hTWhOKBOB87SE4hvlC8OkbRT0mEaW1a/wIS9f4HnYDa/ycRLMV+nvdvYMZur4isNPam44oiRqi7GcILkg==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@loaders.gl/images/-/images-4.3.3.tgz", + "integrity": "sha512-s4InjIXqEu0T7anZLj4OBUuDBt2BNnAD0GLzSexSkBfQZfpXY0XJNl4mMf5nUKb5NDfXhIKIqv8y324US+I28A==", + "license": "MIT", "dependencies": { - "@loaders.gl/loader-utils": "3.4.14" + "@loaders.gl/loader-utils": "4.3.3" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" } }, "node_modules/@loaders.gl/loader-utils": { - "version": "3.4.14", - "resolved": "https://registry.npmjs.org/@loaders.gl/loader-utils/-/loader-utils-3.4.14.tgz", - "integrity": "sha512-HCTY2/F83RLbZWcTvWLVJ1vke3dl6Bye20HU1AqkA37J2vzHwOZ8kj6eee8eeSkIkf7VIFwjyhVJxe0flQE/Bw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@loaders.gl/loader-utils/-/loader-utils-4.3.3.tgz", + "integrity": "sha512-8erUIwWLiIsZX36fFa/seZsfTsWlLk72Sibh/YZJrPAefuVucV4mGGzMBZ96LE2BUfJhadn250eio/59TUFbNw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.3.1", - "@loaders.gl/worker-utils": "3.4.14", - "@probe.gl/stats": "^4.0.1" + "@loaders.gl/schema": "4.3.3", + "@loaders.gl/worker-utils": "4.3.3", + "@probe.gl/log": "^4.0.2", + "@probe.gl/stats": "^4.0.2" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" } }, - "node_modules/@loaders.gl/loader-utils/node_modules/@probe.gl/stats": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@probe.gl/stats/-/stats-4.0.4.tgz", - "integrity": "sha512-SDuSY/D4yDL6LQDa69l/GCcnZLRiGYdyvYkxWb0CgnzTPdPrcdrzGkzkvpC3zsA4fEFw2smlDje370QGHwlisg==", + "node_modules/@loaders.gl/schema": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@loaders.gl/schema/-/schema-4.3.3.tgz", + "integrity": "sha512-zacc9/8je+VbuC6N/QRfiTjRd+BuxsYlddLX1u5/X/cg9s36WZZBlU1oNKUgTYe8eO6+qLyYx77yi+9JbbEehw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.0.0" + "@types/geojson": "^7946.0.7" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" } }, "node_modules/@loaders.gl/worker-utils": { - "version": "3.4.14", - "resolved": "https://registry.npmjs.org/@loaders.gl/worker-utils/-/worker-utils-3.4.14.tgz", - "integrity": "sha512-PUSwxoAYbskisXd0KfYEQ902b0igBA2UAWdP6PzPvY+tJmobfh74dTNwrrBQ1rGXQxxmGx6zc6/ksX6mlIzIrg==", - "dependencies": { - "@babel/runtime": "^7.3.1" + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@loaders.gl/worker-utils/-/worker-utils-4.3.3.tgz", + "integrity": "sha512-eg45Ux6xqsAfqPUqJkhmbFZh9qfmYuPfA+34VcLtfeXIwAngeP6o4SrTmm9LWLGUKiSh47anCEV1p7borDgvGQ==", + "license": "MIT", + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" } }, "node_modules/@luma.gl/constants": { - "version": "8.5.21", - "resolved": "https://registry.npmjs.org/@luma.gl/constants/-/constants-8.5.21.tgz", - "integrity": "sha512-aJxayGxTT+IRd1vfpcgD/cKSCiVJjBNiuiChS96VulrmCvkzUOLvYXr42y5qKB4RyR7vOIda5uQprNzoHrhQAA==" + "version": "9.0.27", + "resolved": "https://registry.npmjs.org/@luma.gl/constants/-/constants-9.0.27.tgz", + "integrity": "sha512-NBkMim3u0xt4UDe4e69L6E/pq5XNxfX60GrggJDzfilVRfIbx5XwKhBXTyNjjtNEk4oc6uYLHWd/05jGRHcfLg==", + "license": "MIT" }, "node_modules/@luma.gl/core": { - "version": "8.5.21", - "resolved": "https://registry.npmjs.org/@luma.gl/core/-/core-8.5.21.tgz", - "integrity": "sha512-11jQJQEMoR/IN2oIsd4zFxiQJk6FE+xgVIMUcsCTBuzafTtQZ8Po9df8mt+MVewpDyBlTVs6g8nxHRH4np1ukA==", + "version": "9.0.27", + "resolved": "https://registry.npmjs.org/@luma.gl/core/-/core-9.0.27.tgz", + "integrity": "sha512-7OXM8ZknTuqt10nL8XHg3YzaHESzU2pSh+6BknLJbLM+UjNWOkDHArF6pRYu96Om0QsnOMK/RXKqXBr+Ni0gvw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.0.0", - "@luma.gl/constants": "8.5.21", - "@luma.gl/engine": "8.5.21", - "@luma.gl/gltools": "8.5.21", - "@luma.gl/shadertools": "8.5.21", - "@luma.gl/webgl": "8.5.21" + "@math.gl/types": "^4.0.0", + "@probe.gl/env": "^4.0.2", + "@probe.gl/log": "^4.0.2", + "@probe.gl/stats": "^4.0.2", + "@types/offscreencanvas": "^2019.6.4" } }, "node_modules/@luma.gl/engine": { - "version": "8.5.21", - "resolved": "https://registry.npmjs.org/@luma.gl/engine/-/engine-8.5.21.tgz", - "integrity": "sha512-IG3WQSKXFNUEs8QG7ZjHtGiOtsakUu+BAxtJ6997A6/F06yynZ44tPe5NU70jG9Yfu3kV0LykPZg7hO3vXZDiA==", - "dependencies": { - "@babel/runtime": "^7.0.0", - "@luma.gl/constants": "8.5.21", - "@luma.gl/gltools": "8.5.21", - "@luma.gl/shadertools": "8.5.21", - "@luma.gl/webgl": "8.5.21", - "@math.gl/core": "^3.5.0", - "@probe.gl/env": "^3.5.0", - "@probe.gl/stats": "^3.5.0", - "@types/offscreencanvas": "^2019.7.0" - } - }, - "node_modules/@luma.gl/gltools": { - "version": "8.5.21", - "resolved": "https://registry.npmjs.org/@luma.gl/gltools/-/gltools-8.5.21.tgz", - "integrity": "sha512-6qZ0LaT2Mxa4AJT5F44TFoaziokYiHUwO45vnM/NYUOIu9xevcmS6VtToawytMEACGL6PDeDyVqP3Y80SDzq5g==", + "version": "9.0.27", + "resolved": "https://registry.npmjs.org/@luma.gl/engine/-/engine-9.0.27.tgz", + "integrity": "sha512-O4e7RbIjBJX5WLs8HJLjpccYEkcans4pz8+TI8Y7BO7gDq9ZbEASbVd5CT53jFLfTjnRuqAOpElfaXwQ/B7oWg==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.0.0", - "@luma.gl/constants": "8.5.21", - "@probe.gl/env": "^3.5.0", - "@probe.gl/log": "^3.5.0", - "@types/offscreencanvas": "^2019.7.0" + "@luma.gl/shadertools": "9.0.27", + "@math.gl/core": "^4.0.0", + "@probe.gl/log": "^4.0.2", + "@probe.gl/stats": "^4.0.2" + }, + "peerDependencies": { + "@luma.gl/core": "^9.0.0" } }, "node_modules/@luma.gl/shadertools": { - "version": "8.5.21", - "resolved": "https://registry.npmjs.org/@luma.gl/shadertools/-/shadertools-8.5.21.tgz", - "integrity": "sha512-WQah7yFDJ8cNCLPYpIm3r0wSlXLvjoA279fcknmATvvkW3/i8PcCJ/nYEBJO3hHEwwMQxD16+YZu/uwGiifLMg==", + "version": "9.0.27", + "resolved": "https://registry.npmjs.org/@luma.gl/shadertools/-/shadertools-9.0.27.tgz", + "integrity": "sha512-JcOuYH2Fh4uljinXKbR04en1dqEthlJNdqV5efQ0fE9NetJul7Pkq+N1v/Oo8/vmJn9ZqEC49dgZHwtbzY8UnQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.0.0", - "@math.gl/core": "^3.5.0" + "@math.gl/core": "^4.0.0", + "@math.gl/types": "^4.0.0", + "wgsl_reflect": "^1.0.1" + }, + "peerDependencies": { + "@luma.gl/core": "^9.0.0" } }, "node_modules/@luma.gl/webgl": { - "version": "8.5.21", - "resolved": "https://registry.npmjs.org/@luma.gl/webgl/-/webgl-8.5.21.tgz", - "integrity": "sha512-ZVLO4W5UuaOlzZIwmFWhnmZ1gYoU97a+heMqxLrSSmCUAsSu3ZETUex9gOmzdM1WWxcdWaa3M68rvKCNEgwz0Q==", + "version": "9.0.27", + "resolved": "https://registry.npmjs.org/@luma.gl/webgl/-/webgl-9.0.27.tgz", + "integrity": "sha512-GOzOiDfTFgT4If1XSeCqXswKrgXVwTyuf/1W21Vv7fs5inub5p3LISmZglrt/RcdaGyXQQ5zEqf/+x67dGTeYw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.0.0", - "@luma.gl/constants": "8.5.21", - "@luma.gl/gltools": "8.5.21", - "@probe.gl/env": "^3.5.0", - "@probe.gl/stats": "^3.5.0" + "@luma.gl/constants": "9.0.27", + "@probe.gl/env": "^4.0.2" + }, + "peerDependencies": { + "@luma.gl/core": "^9.0.0" } }, "node_modules/@material-ui/types": { @@ -1316,35 +1320,33 @@ } }, "node_modules/@math.gl/core": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/@math.gl/core/-/core-3.6.3.tgz", - "integrity": "sha512-jBABmDkj5uuuE0dTDmwwss7Cup5ZwQ6Qb7h1pgvtkEutTrhkcv8SuItQNXmF45494yIHeoGue08NlyeY6wxq2A==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@math.gl/core/-/core-4.1.0.tgz", + "integrity": "sha512-FrdHBCVG3QdrworwrUSzXIaK+/9OCRLscxI2OUy6sLOHyHgBMyfnEGs99/m3KNvs+95BsnQLWklVfpKfQzfwKA==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.12.0", - "@math.gl/types": "3.6.3", - "gl-matrix": "^3.4.0" + "@math.gl/types": "4.1.0" } }, "node_modules/@math.gl/sun": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/@math.gl/sun/-/sun-3.6.3.tgz", - "integrity": "sha512-mrx6CGYYeTNSQttvcw0KVUy+35YDmnjMqpO/o0t06Vcghrt0HNruB/ScRgUSbJrgkbOg1Vcqm23HBd++clzQzw==", - "dependencies": { - "@babel/runtime": "^7.12.0" - } + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@math.gl/sun/-/sun-4.1.0.tgz", + "integrity": "sha512-i3q6OCBLSZ5wgZVhXg+X7gsjY/TUtuFW/2KBiq/U1ypLso3S4sEykoU/MGjxUv1xiiGtr+v8TeMbO1OBIh/HmA==", + "license": "MIT" }, "node_modules/@math.gl/types": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/@math.gl/types/-/types-3.6.3.tgz", - "integrity": "sha512-3uWLVXHY3jQxsXCr/UCNPSc2BG0hNUljhmOBt9l+lNFDp7zHgm0cK2Tw4kj2XfkJy4TgwZTBGwRDQgWEbLbdTA==" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@math.gl/types/-/types-4.1.0.tgz", + "integrity": "sha512-clYZdHcmRvMzVK5fjeDkQlHUzXQSNdZ7s4xOqC3nJPgz4C/TZkUecTo9YS4PruZqtDda/ag4erndP0MIn40dGA==", + "license": "MIT" }, "node_modules/@math.gl/web-mercator": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/@math.gl/web-mercator/-/web-mercator-3.6.3.tgz", - "integrity": "sha512-UVrkSOs02YLehKaehrxhAejYMurehIHPfFQvPFZmdJHglHOU4V2cCUApTVEwOksvCp161ypEqVp+9H6mGhTTcw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@math.gl/web-mercator/-/web-mercator-4.1.0.tgz", + "integrity": "sha512-HZo3vO5GCMkXJThxRJ5/QYUYRr3XumfT8CzNNCwoJfinxy5NtKUd7dusNTXn7yJ40UoB8FMIwkVwNlqaiRZZAw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.12.0", - "gl-matrix": "^3.4.0" + "@math.gl/core": "4.1.0" } }, "node_modules/@mui/base": { @@ -1835,29 +1837,25 @@ } }, "node_modules/@probe.gl/env": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@probe.gl/env/-/env-3.6.0.tgz", - "integrity": "sha512-4tTZYUg/8BICC3Yyb9rOeoKeijKbZHRXBEKObrfPmX4sQmYB15ZOUpoVBhAyJkOYVAM8EkPci6Uw5dLCwx2BEQ==", - "dependencies": { - "@babel/runtime": "^7.0.0" - } + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@probe.gl/env/-/env-4.0.9.tgz", + "integrity": "sha512-AOmVMD0/j78mX+k4+qX7ZhE0sY9H+EaJgIO6trik0BwV6VcrwxTGCGFAeuRsIGhETDnye06tkLXccYatYxAYwQ==", + "license": "MIT" }, "node_modules/@probe.gl/log": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@probe.gl/log/-/log-3.6.0.tgz", - "integrity": "sha512-hjpyenpEvOdowgZ1qMeCJxfRD4JkKdlXz0RC14m42Un62NtOT+GpWyKA4LssT0+xyLULCByRAtG2fzZorpIAcA==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@probe.gl/log/-/log-4.0.9.tgz", + "integrity": "sha512-ebuZaodSRE9aC+3bVC7cKRHT8garXeT1jTbj1R5tQRqQYc9iGeT3iemVOHx5bN9Q6gAs/0j54iPI+1DvWMAW4A==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.0.0", - "@probe.gl/env": "3.6.0" + "@probe.gl/env": "4.0.9" } }, "node_modules/@probe.gl/stats": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@probe.gl/stats/-/stats-3.6.0.tgz", - "integrity": "sha512-JdALQXB44OP4kUBN/UrQgzbJe4qokbVF4Y8lkIA8iVCFnjVowWIgkD/z/0QO65yELT54tTrtepw1jScjKB+rhQ==", - "dependencies": { - "@babel/runtime": "^7.0.0" - } + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@probe.gl/stats/-/stats-4.0.9.tgz", + "integrity": "sha512-Q9Xt/sJUQaMsbjRKjOscv2t7wXIymTrOEJ4a3da4FTCn7bkKvcdxdyFAQySCrtPxE+YZ5I5lXpWPgv9BwmpE1g==", + "license": "MIT" }, "node_modules/@react-hook/latest": { "version": "1.0.3", @@ -2254,8 +2252,7 @@ "node_modules/@types/geojson": { "version": "7946.0.11", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.11.tgz", - "integrity": "sha512-L7A0AINMXQpVwxHJ4jxD6/XjZ4NDufaRlUJHjNIFKYUFBH1SvOW+neaqb0VTRSLW5suSrSu19ObFEFnfNcr+qg==", - "dev": true + "integrity": "sha512-L7A0AINMXQpVwxHJ4jxD6/XjZ4NDufaRlUJHjNIFKYUFBH1SvOW+neaqb0VTRSLW5suSrSu19ObFEFnfNcr+qg==" }, "node_modules/@types/hammerjs": { "version": "2.0.42", @@ -2347,9 +2344,10 @@ "license": "MIT" }, "node_modules/@types/offscreencanvas": { - "version": "2019.7.1", - "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.1.tgz", - "integrity": "sha512-+HSrJgjBW77ALieQdMJvXhRZUIRN1597L+BKvsyeiIlHHERnqjcuOLyodK3auJ3Y3zRezNKtKAhuQWYJfEgFHQ==" + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" }, "node_modules/@types/parse-json": { "version": "4.0.2", @@ -2361,11 +2359,11 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.2.22", - "license": "MIT", + "version": "18.3.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", + "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", "dependencies": { "@types/prop-types": "*", - "@types/scheduler": "*", "csstype": "^3.0.2" } }, @@ -2387,7 +2385,9 @@ }, "node_modules/@types/scheduler": { "version": "0.16.3", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@types/semver": { "version": "7.5.2", @@ -6099,7 +6099,8 @@ "node_modules/gl-matrix": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", - "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" + "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==", + "license": "MIT" }, "node_modules/glob": { "version": "7.2.3", @@ -7623,14 +7624,6 @@ "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" }, - "node_modules/math.gl": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/math.gl/-/math.gl-3.6.3.tgz", - "integrity": "sha512-Yq9CyECvSDox9+5ETi2+x1bGTY5WvGUGL3rJfC4KPoCZAM51MGfrCm6rIn4yOJUVfMPs2a5RwMD+yGS/n1g3gg==", - "dependencies": { - "@math.gl/core": "3.6.3" - } - }, "node_modules/mathjs": { "version": "9.5.2", "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-9.5.2.tgz", @@ -9567,8 +9560,9 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react": { - "version": "18.2.0", - "license": "MIT", + "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==", "dependencies": { "loose-envify": "^1.1.0" }, @@ -9667,14 +9661,15 @@ } }, "node_modules/react-dom": { - "version": "18.2.0", - "license": "MIT", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dependencies": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.3.1" } }, "node_modules/react-draggable": { @@ -10296,8 +10291,9 @@ } }, "node_modules/scheduler": { - "version": "0.23.0", - "license": "MIT", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dependencies": { "loose-envify": "^1.1.0" } @@ -12016,6 +12012,12 @@ "node": ">=10.13.0" } }, + "node_modules/wgsl_reflect": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/wgsl_reflect/-/wgsl_reflect-1.0.16.tgz", + "integrity": "sha512-OE3urfXXbHMD5lhKZwxOxC9SFYynEGEkWXQmvi7B1gzzr5jb9+drh9A8MeBvVqKqznCoBuh8WOzVuSGSZs4CkQ==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "dev": true, diff --git a/python/package.json b/python/package.json index 46112a02f3..54326f1881 100644 --- a/python/package.json +++ b/python/package.json @@ -28,7 +28,8 @@ "validate": "npm run typecheck && npm run lint" }, "dependencies": { - "@deck.gl/core": "^8.9.35", + "@deck.gl/core": "^9.0.36", + "@deck.gl/react": "^9.0.36", "@emerson-eps/color-tables": "^0.4.85", "@equinor/eds-core-react": "0.33.0", "@equinor/eds-icons": "^0.19.1", @@ -45,8 +46,8 @@ "leaflet-draw": "^1.0.4", "lodash": "^4.17.21", "mathjs": "^9.4.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-dropdown-tree-select": "^2.8.0", "react-redux": "^8.1.1", "react-resize-detector": "^9.0.0" @@ -61,7 +62,7 @@ "@types/leaflet": "^1.8.0", "@types/leaflet-draw": "^1.0.8", "@types/lodash": "^4.14.199", - "@types/react": "^18.2.7", + "@types/react": "^18.3.12", "@types/react-dom": "^18.2.7", "@typescript-eslint/eslint-plugin": "^6.4.0", "@typescript-eslint/parser": "^6.4.0", diff --git a/python/src/components/SubsurfaceViewer/SubsurfaceViewer.tsx b/python/src/components/SubsurfaceViewer/SubsurfaceViewer.tsx index b2b2b4d624..3b5ca72d19 100644 --- a/python/src/components/SubsurfaceViewer/SubsurfaceViewer.tsx +++ b/python/src/components/SubsurfaceViewer/SubsurfaceViewer.tsx @@ -1,21 +1,209 @@ import React from "react"; -import { SubsurfaceViewerProps } from "@webviz/subsurface-viewer"; +import { + MapMouseEvent, + SubsurfaceViewerProps, + ViewStateType, +} from "@webviz/subsurface-viewer"; +import { DeckGLRef } from "@deck.gl/react"; +import { PickingInfoPerView, useMultiViewPicking } from "@webviz/subsurface-viewer/src/hooks/useMultiViewPicking"; +import { useMultiViewCursorTracking } from "@webviz/subsurface-viewer/src/hooks/useMultiViewCursorTracking"; +import { isEqual } from "lodash"; +import ViewAnnotation from "../ViewAnnotation/ViewAnnotation"; -const SubsurfaceViewerComponent = React.lazy( - () => - import( - /* webpackChunkName: "webviz-subsurface-viewer" */ "@webviz/subsurface-viewer" - ) +const SubsurfaceViewerComponent = React.lazy(() => + import( + /* webpackChunkName: "webviz-subsurface-viewer" */ "@webviz/subsurface-viewer" + ).then((module) => ({ + default: + module.DashSubsurfaceViewer as unknown as React.ComponentType, + })) ); const SubsurfaceViewer: React.FC = (props) => { + const { views, children, ...rest } = props; + + if (!views) { + return ( + Loading...}> + + {props.children} + + + ); + } + return ( Loading...}> - + + {children} + ); }; +function MultiViewSubsurfaceViewer( + props: SubsurfaceViewerProps & + Required> +) { + const { onMouseEvent, getCameraPosition } = props; + + const deckGlRef = React.useRef(null); + + const [mouseHover, setMouseHover] = React.useState(false); + const [cameraPosition, setCameraPosition] = React.useState< + ViewStateType | undefined + >(undefined); + const [prevCameraPosition, setPrevCameraPosition] = React.useState< + ViewStateType | undefined + >(undefined); + + if (!isEqual(prevCameraPosition, props.cameraPosition)) { + setPrevCameraPosition(props.cameraPosition); + } + + const { getPickingInfo, activeViewportId, pickingInfoPerView } = + useMultiViewPicking({ + deckGlRef, + multiPicking: true, + pickDepth: 1, + }); + + const handleMouseEvent = React.useCallback( + function handleMouseEvent(event: MapMouseEvent) { + if (event.type === "hover") { + getPickingInfo(event); + } + onMouseEvent?.(event); + }, + [getPickingInfo, onMouseEvent] + ); + + const handleCameraPositionChange = React.useCallback( + function handleCameraPositionChange(position: ViewStateType) { + setCameraPosition(position); + getCameraPosition?.(position); + }, + [getCameraPosition] + ); + + const viewports = props.views?.viewports ?? []; + const layers = props.layers ?? []; + + const { viewports: adjustedViewports, layers: adjustedLayers } = + useMultiViewCursorTracking({ + activeViewportId, + worldCoordinates: + pickingInfoPerView[activeViewportId]?.coordinates ?? null, + viewports, + layers, + crosshairProps: { + color: [255, 255, 255, 255], + sizePx: 32, + visible: mouseHover, + }, + }); + + const children = React.Children.toArray(props.children); + for (const viewport of adjustedViewports) { + children.push( + + + + ); + } + + return ( +
setMouseHover(true)} + onMouseLeave={() => setMouseHover(false)} + onBlur={() => setMouseHover(false)} + onFocus={() => setMouseHover(true)} + > + + {children} + +
+ ); +} + +function ReadoutComponent(props: { + viewId: string; + pickingInfoPerView: PickingInfoPerView; +}): React.ReactNode { + return ( +
+
X:
+
+ {roundToSignificant(props.pickingInfoPerView[props.viewId]?.coordinates + ?.at(0))} +
+
Y:
+
+ {roundToSignificant(props.pickingInfoPerView[props.viewId]?.coordinates + ?.at(1))} +
+ {props.pickingInfoPerView[props.viewId]?.layerPickingInfo.map( + (el) => ( + +
{el.layerName}
+ {el.properties.map((prop, i) => ( + +
{prop.name}
+
+ {typeof prop.value === "string" + ? prop.value + : roundToSignificant(prop.value)} +
+
+ ))} +
+ ) + ) ?? ""} +
+ ); +} + +const roundToSignificant = function (num: number | undefined) { + if (num === undefined) { + return "-"; + } + // Returns two significant figures (non-zero) for numbers with an absolute value less + // than 1, and two decimal places for numbers with an absolute value greater + // than 1. + return parseFloat( + num.toExponential(Math.max(1, 2 + Math.log10(Math.abs(num)))) + ); +}; + SubsurfaceViewer.displayName = "SubsurfaceViewer"; export default SubsurfaceViewer; diff --git a/python/src/components/WellLogViewer/WellLogViewer.tsx b/python/src/components/WellLogViewer/WellLogViewer.tsx index febb4474e1..05de630744 100644 --- a/python/src/components/WellLogViewer/WellLogViewer.tsx +++ b/python/src/components/WellLogViewer/WellLogViewer.tsx @@ -9,14 +9,9 @@ const WellLogViewerComponent = React.lazy(() => })) ); -import type { ColorMapFunction } from "../components/ColorMapFunction"; -import type { WellPickProps } from "../components/WellLogView"; // react-docgen / dash-generate-components/extract-meta.js does not properly parse // the imported WellLogViewerProps. Hence, we have to recreate them here. -/** - * WellLogView additional options - */ type WellLogViewOptions = { /** The maximum zoom value */ maxContentZoom?: number; @@ -56,7 +51,7 @@ type WellLogViewerProps = { template: object; /** Prop containing color function/table array */ - colorMapFunctions: ColorMapFunction[]; + colorMapFunctions: any; /** Orientation of the track plots on the screen. Default is false */ horizontal?: boolean; @@ -68,7 +63,7 @@ type WellLogViewerProps = { selection?: number[]; /** Well picks data */ - wellpick?: WellPickProps; + wellpick?: any; /** Primary axis id: " md", "tvd", "time"... */ primaryAxis?: string; diff --git a/typescript/packages/subsurface-viewer/src/components/View.tsx b/typescript/packages/subsurface-viewer/src/components/View.tsx new file mode 100644 index 0000000000..f852d186ee --- /dev/null +++ b/typescript/packages/subsurface-viewer/src/components/View.tsx @@ -0,0 +1,29 @@ +import { Deck, Viewport, View } from "@deck.gl/core"; +import { DeckGLRef } from "@deck.gl/react"; +import React from "react"; + +export type ViewProps = { + id: string; + deckGlRef: React.RefObject; + children?: React.ReactNode; +}; + +export function ViewContent(props: ViewProps): React.ReactNode { + const deckRef = React.useRef(null); + + React.useEffect( + function onMount() { + if (props.deckGlRef.current) { + deckRef.current = props.deckGlRef.current.deck ?? null; + if (deckRef.current) { + const views = deckRef.current.getViews(); + const view = views.find((v) => v.id === props.id); + if (view) { + deckRef.current.props.onViewStateChange + } + } + } + }, + [props.deckGlRef] + ); +} \ No newline at end of file diff --git a/typescript/packages/subsurface-viewer/src/hooks/useMultiViewCursorTracking/index.ts b/typescript/packages/subsurface-viewer/src/hooks/useMultiViewCursorTracking/index.ts new file mode 100644 index 0000000000..c397990098 --- /dev/null +++ b/typescript/packages/subsurface-viewer/src/hooks/useMultiViewCursorTracking/index.ts @@ -0,0 +1,6 @@ +export { useMultiViewCursorTracking } from "./useMultiViewCursorTracking"; + +export type { + UseMultiViewCursorTrackingProps, + UseMultiViewCursorTrackingReturnType, +} from "./useMultiViewCursorTracking"; diff --git a/typescript/packages/subsurface-viewer/src/hooks/useMultiViewCursorTracking/useMultiViewCursorTracking.ts b/typescript/packages/subsurface-viewer/src/hooks/useMultiViewCursorTracking/useMultiViewCursorTracking.ts new file mode 100644 index 0000000000..b483424101 --- /dev/null +++ b/typescript/packages/subsurface-viewer/src/hooks/useMultiViewCursorTracking/useMultiViewCursorTracking.ts @@ -0,0 +1,62 @@ +import React from "react"; +import { v4 } from "uuid"; +import { CrosshairLayer } from "../../layers"; + +import type { CrosshairLayerProps } from "../../layers"; +import type { ViewportType } from "../../views/viewport"; +import type { TLayerDefinition } from "../../SubsurfaceViewer"; + +export type UseMultiViewCursorTrackingProps = { + worldCoordinates: number[] | null; + crosshairProps?: Omit; + viewports: ViewportType[]; + layers: TLayerDefinition[]; + activeViewportId: string; +}; + +export type UseMultiViewCursorTrackingReturnType = { + viewports: ViewportType[]; + layers: TLayerDefinition[]; +}; + +export function useMultiViewCursorTracking( + props: UseMultiViewCursorTrackingProps +): UseMultiViewCursorTrackingReturnType { + const id = React.useRef(`crosshair-${v4()}`); + + let worldCoordinates: [number, number, number] | null = null; + if (props.worldCoordinates?.length === 3) { + worldCoordinates = props.worldCoordinates as [number, number, number]; + } + if (props.worldCoordinates?.length === 2) { + worldCoordinates = [...props.worldCoordinates, 0] as [ + number, + number, + number, + ]; + } + + const crosshairLayer = new CrosshairLayer({ + id: id.current, + worldCoordinates, + ...props.crosshairProps, + }); + + const layers = [...props.layers, crosshairLayer]; + + const viewports = props.viewports.map((viewport) => { + if (viewport.id !== props.activeViewportId) { + return { + ...viewport, + layerIds: [...(viewport.layerIds ?? []), id.current], + }; + } + + return viewport; + }); + + return { + viewports, + layers, + }; +} diff --git a/typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/MultiViewPickingInfoAssembler.ts b/typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/MultiViewPickingInfoAssembler.ts new file mode 100644 index 0000000000..1b7757f876 --- /dev/null +++ b/typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/MultiViewPickingInfoAssembler.ts @@ -0,0 +1,208 @@ +import type { PickingInfo, Viewport } from "@deck.gl/core"; +import { DeckGLRef } from "@deck.gl/react"; +import { ExtendedLayerProps, MapMouseEvent, PropertyDataType } from "../../"; + +export type LayerPickingInfo = { + layerId: string; + layerName: string; + properties: PropertyDataType[]; +}; + +export type PickingInfoPerView = Record< + string, + { + coordinates: number[] | null; + layerPickingInfo: LayerPickingInfo[]; + } +>; + +function hasPropertiesArray(obj: any): obj is { properties: PropertyDataType[] } { + return obj && Array.isArray(obj.properties); +} + +function hasSingleProperty(obj: any): obj is { propertyValue: number } { + return obj && "propertyValue" in obj && typeof obj.propertyValue === "number"; +} + +export type MultiViewPickingInfoAssemblerOptions = { + multiPicking: boolean; + pickDepth: number; +}; + +export interface MultiViewPickingInfoAssemblerSubscriberCallback { + (info: PickingInfoPerView, activeViewportId: string): void; +} + +export class MultiViewPickingInfoAssembler { + private _deckGl: DeckGLRef | null = null; + private _options: MultiViewPickingInfoAssemblerOptions; + private _subscribers: Set = new Set(); + + constructor( + deckGL: DeckGLRef | null, + options: MultiViewPickingInfoAssemblerOptions = { multiPicking: false, pickDepth: 1 } + ) { + this._deckGl = deckGL; + this._options = options; + } + + setDeckGL(deckGL: DeckGLRef) { + this._deckGl = deckGL; + } + + subscribe(callback: MultiViewPickingInfoAssemblerSubscriberCallback): () => void { + this._subscribers.add(callback); + + return () => { + this._subscribers.delete(callback); + }; + } + + private publish(info: PickingInfoPerView, activeViewportId: string) { + for (const subscriber of this._subscribers) { + subscriber(info, activeViewportId); + } + } + + getMultiViewPickingInfo(hoverEvent: MapMouseEvent) { + if (!this._deckGl?.deck) { + return; + } + + const viewports = this._deckGl.deck?.getViewports(); + if (!viewports) { + return; + } + + if (hoverEvent.infos.length === 0) { + return; + } + + const activeViewportId = hoverEvent.infos[0].viewport?.id; + + if (!activeViewportId) { + return; + } + + const eventScreenCoordinate: [number, number] = [hoverEvent.infos[0].x, hoverEvent.infos[0].y]; + + this.assembleMultiViewPickingInfo(eventScreenCoordinate, activeViewportId, viewports).then((info) => { + this.publish(info, activeViewportId); + }); + } + + private async assembleMultiViewPickingInfo( + eventScreenCoordinate: [number, number], + activeViewportId: string, + viewports: Viewport[] + ): Promise { + return new Promise((resolve, reject) => { + const deck = this._deckGl?.deck; + if (!deck) { + reject("DeckGL not initialized"); + return; + } + const activeViewport = viewports.find((el) => el.id === activeViewportId); + if (!activeViewport) { + reject("Active viewport not found"); + return; + } + + const activeViewportRelativeScreenCoordinates: [number, number] = [ + eventScreenCoordinate[0] - activeViewport.x, + eventScreenCoordinate[1] - activeViewport.y, + ]; + + const worldCoordinate = activeViewport.unproject(activeViewportRelativeScreenCoordinates); + + const collectedPickingInfo: PickingInfoPerView = {}; + for (const viewport of viewports) { + const [relativeScreenX, relativeScreenY] = viewport.project(worldCoordinate); + + let pickingInfo: PickingInfo[] = []; + if (this._options.multiPicking) { + pickingInfo = deck.pickMultipleObjects({ + x: relativeScreenX + viewport.x, + y: relativeScreenY + viewport.y, + depth: this._options.pickDepth, + unproject3D: true, + }); + } else { + const obj = deck.pickObject({ + x: relativeScreenX + viewport.x, + y: relativeScreenY + viewport.y, + unproject3D: true, + }); + pickingInfo = obj ? [obj] : []; + } + + if (pickingInfo) { + const collectedLayerPickingInfo: LayerPickingInfo[] = []; + for (const info of pickingInfo) { + const hasMultipleProperties = hasPropertiesArray(info); + const hasOneProperty = hasSingleProperty(info); + + if (!hasMultipleProperties && !hasOneProperty) { + continue; + } + + if (!info.layer) { + continue; + } + + if (collectedLayerPickingInfo.find((el) => el.layerId === info.layer?.id)) { + continue; + } + + const layerId = info.layer.id; + const layerName = (info.layer.props as unknown as ExtendedLayerProps).name; + + let layerPickingInfo = collectedLayerPickingInfo.find((el) => el.layerId === layerId); + + if (!layerPickingInfo) { + collectedLayerPickingInfo.push({ + layerId, + layerName, + properties: [], + }); + layerPickingInfo = collectedLayerPickingInfo[collectedLayerPickingInfo.length - 1]; + } + + if (hasOneProperty) { + layerPickingInfo.properties.push({ + name: "Value", + value: info.propertyValue, + color: undefined, + }); + continue; + } + + if (hasMultipleProperties) { + const properties = info.properties; + + for (const property of properties) { + layerPickingInfo.properties.push({ + name: property.name, + value: property.value, + color: property.color, + }); + } + } + } + + collectedPickingInfo[viewport.id] = { + coordinates: worldCoordinate, + layerPickingInfo: collectedLayerPickingInfo, + }; + } else { + collectedPickingInfo[viewport.id] = { + coordinates: null, + layerPickingInfo: [], + }; + } + } + + resolve(collectedPickingInfo); + }); + } +} \ No newline at end of file diff --git a/typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/index.ts b/typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/index.ts new file mode 100644 index 0000000000..7c9661b5b2 --- /dev/null +++ b/typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/index.ts @@ -0,0 +1,3 @@ +export { useMultiViewPicking } from './useMultiViewPicking'; +export type { UseMultiViewPickingProps, UseMultiViewPickingReturnType } from './useMultiViewPicking'; +export type { PickingInfoPerView, LayerPickingInfo} from './MultiViewPickingInfoAssembler'; \ No newline at end of file diff --git a/typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/useMultiViewPicking.ts b/typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/useMultiViewPicking.ts new file mode 100644 index 0000000000..00b7c38000 --- /dev/null +++ b/typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/useMultiViewPicking.ts @@ -0,0 +1,79 @@ +import React from "react"; + +import { MultiViewPickingInfoAssembler } from "./MultiViewPickingInfoAssembler"; + +import type { DeckGLRef } from "@deck.gl/react"; +import type { PickingInfoPerView } from "./MultiViewPickingInfoAssembler"; +import type { MapMouseEvent } from "../../SubsurfaceViewer"; + +export type UseMultiViewPickingProps = { + deckGlRef: React.RefObject; + multiPicking: boolean; + pickDepth: number; +}; + +export type UseMultiViewPickingReturnType = { + getPickingInfo: (event: MapMouseEvent) => void; + pickingInfoPerView: PickingInfoPerView; + activeViewportId: string; +}; + +// Make a documentation comment for the function +/** + * Hook for multi-view picking. + * + * @param props - The hook properties. + * @returns The hook return value. + */ +export function useMultiViewPicking( + props: UseMultiViewPickingProps +): UseMultiViewPickingReturnType { + const assembler = React.useRef(null); + + const [info, setInfo] = React.useState< + Omit + >({ + pickingInfoPerView: {}, + activeViewportId: "", + }); + + React.useEffect( + function onPropsChangeEffect() { + assembler.current = new MultiViewPickingInfoAssembler( + props.deckGlRef.current, + { multiPicking: props.multiPicking, pickDepth: props.pickDepth } + ); + + const unsubscribe = assembler.current.subscribe( + function onPickingInfo( + info: PickingInfoPerView, + activeViewportId: string + ) { + setInfo({ + pickingInfoPerView: info, + activeViewportId, + }); + } + ); + + return function onPropsChangeUnmountEffect() { + assembler.current = null; + unsubscribe(); + }; + }, + [props.deckGlRef, props.multiPicking, props.pickDepth] + ); + + const getPickingInfo = React.useCallback(function getPickingInfo( + event: MapMouseEvent + ) { + if (assembler.current) { + assembler.current.getMultiViewPickingInfo(event); + } + }, []); + + return { + getPickingInfo, + ...info, + }; +} diff --git a/typescript/packages/subsurface-viewer/src/layers/crosshair/crosshairLayer.ts b/typescript/packages/subsurface-viewer/src/layers/crosshair/crosshairLayer.ts new file mode 100644 index 0000000000..22bde667a6 --- /dev/null +++ b/typescript/packages/subsurface-viewer/src/layers/crosshair/crosshairLayer.ts @@ -0,0 +1,68 @@ +import { CompositeLayer, DefaultProps } from "@deck.gl/core"; +import { IconLayer } from "@deck.gl/layers"; + +function makeCrossHairSvg(color: [number, number, number, number]): string { + return ` + + + + + + +`; +} + +export type CrosshairLayerProps = { + id: string; + worldCoordinates: [number, number, number] | null; + color?: [number, number, number, number]; + sizePx?: number; + visible?: boolean; +}; + +const defaultProps: DefaultProps = { + color: [0, 0, 0, 0.5], + sizePx: 24, + visible: true, +}; + +class CrosshairLayer extends CompositeLayer { + static layerName: string = "CrosshairLayer"; + static defaultProps = defaultProps; + + renderLayers() { + if (!this.props.worldCoordinates || !this.props.visible) { + return []; + } + + return [ + new IconLayer( + this.getSubLayerProps({ + id: "crosshair-icon-layer", + getIcon: () => ({ + width: 150, + height: 150, + url: svgToDataURL( + makeCrossHairSvg(this.props.color || [0, 0, 0, 255]) + ), + }), + data: this.props.worldCoordinates + ? [this.props.worldCoordinates] + : [], + getPosition: (d: [number, number, number]) => d, + getSize: this.props.sizePx, + sizeUnits: "pixels", + getColor: () => this.props.color || [0, 0, 0, 255], + pickable: false, + }) + ), + ]; + } +} + +function svgToDataURL(svg: string): string { + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; +} + +export default CrosshairLayer; diff --git a/typescript/packages/subsurface-viewer/src/layers/index.ts b/typescript/packages/subsurface-viewer/src/layers/index.ts index eace9470f0..e7a4552a12 100644 --- a/typescript/packages/subsurface-viewer/src/layers/index.ts +++ b/typescript/packages/subsurface-viewer/src/layers/index.ts @@ -16,6 +16,7 @@ export { default as NorthArrow3DLayer } from "./northarrow/northArrow3DLayer"; export { default as UnfoldedGeoJsonLayer } from "./intersection/unfoldedGeoJsonLayer"; export { default as Grid3DLayer } from "./grid3d/grid3dLayer"; export { default as BoxSelectionLayer } from "./BoxSelectionLayer/boxSelectionLayer"; +export { default as CrosshairLayer } from "./crosshair/crosshairLayer"; export type { AxesLayerProps } from "./axes/axesLayer"; export type { Axes2DLayerProps } from "./axes2d/axes2DLayer"; @@ -33,6 +34,7 @@ export type { WellsLayerProps } from "./wells/wellsLayer"; export { default as WellMarkersLayerProps } from "./well_markers/wellMarkersLayer"; export type { Grid3DLayerProps } from "./grid3d/grid3dLayer"; export type { BoxSelectionLayerProps } from "./BoxSelectionLayer/boxSelectionLayer"; +export type { CrosshairLayerProps } from "./crosshair/crosshairLayer"; // Export layer utility functions export { abscissaTransform } from "./wells/utils/abscissaTransform"; diff --git a/typescript/packages/subsurface-viewer/src/storybook/examples/MultiViewExamples.stories.tsx b/typescript/packages/subsurface-viewer/src/storybook/examples/MultiViewExamples.stories.tsx index a1365cef98..292e1dfe6f 100644 --- a/typescript/packages/subsurface-viewer/src/storybook/examples/MultiViewExamples.stories.tsx +++ b/typescript/packages/subsurface-viewer/src/storybook/examples/MultiViewExamples.stories.tsx @@ -2,9 +2,10 @@ import type { Meta, StoryObj } from "@storybook/react"; import { fireEvent, userEvent } from "@storybook/test"; import React from "react"; -import type { PickingInfo, Viewport } from "@deck.gl/core"; import { View } from "@deck.gl/core"; -import type { DeckGLRef } from "@deck.gl/react"; +import { useMultiViewPicking } from "../../hooks/useMultiViewPicking"; +import { useMultiViewCursorTracking } from "../../hooks/useMultiViewCursorTracking"; +import type { PickingInfoPerView } from "../../hooks/useMultiViewPicking"; import { ContinuousLegend } from "@emerson-eps/color-tables"; @@ -13,7 +14,7 @@ import Tab from "@mui/material/Tab"; import Tabs from "@mui/material/Tabs"; import type { SubsurfaceViewerProps } from "../../SubsurfaceViewer"; -import SubsurfaceViewer from "../../SubsurfaceViewer"; +import SubsurfaceViewer, { ViewStateType } from "../../SubsurfaceViewer"; import type { MapMouseEvent, ViewsType } from "../../components/Map"; import { ViewFooter } from "../../components/ViewFooter"; @@ -32,6 +33,7 @@ import { redAxes2DLayer, subsufaceProps, } from "../sharedSettings"; +import { DeckGLRef } from "@deck.gl/react"; const stories: Meta = { component: SubsurfaceViewer, @@ -84,172 +86,6 @@ export const MultiViewAnnotation: StoryObj = { ), }; -type PickingInfoProperty = { - name: string; - value: number; - color?: string; -}; - -type PickingInfoPerView = Record< - string, - { - x: number | null; - y: number | null; - properties: PickingInfoProperty[]; - } ->; - -class MultiViewPickingInfoAssembler { - private _deckGl: DeckGLRef | null = null; - private _multiPicking: boolean; - private _subscribers: Set<(info: PickingInfoPerView) => void> = new Set(); - - constructor(deckGL: DeckGLRef | null, multiPicking: boolean = false) { - this._deckGl = deckGL; - this._multiPicking = multiPicking; - } - - setDeckGL(deckGL: DeckGLRef) { - this._deckGl = deckGL; - } - - subscribe(callback: (info: PickingInfoPerView) => void): () => void { - this._subscribers.add(callback); - - return () => { - this._subscribers.delete(callback); - }; - } - - private publish(info: PickingInfoPerView) { - for (const subscriber of this._subscribers) { - subscriber(info); - } - } - - getMultiViewPickingInfo(hoverEvent: MapMouseEvent) { - if (!this._deckGl?.deck) { - return; - } - - const viewports = this._deckGl.deck?.getViewports(); - if (!viewports) { - return; - } - - if (hoverEvent.infos.length === 0) { - return; - } - - const activeViewportId = hoverEvent.infos[0].viewport?.id; - - if (!activeViewportId) { - return; - } - - const eventScreenCoordinate: [number, number] = [ - hoverEvent.infos[0].x, - hoverEvent.infos[0].y, - ]; - - this.assembleMultiViewPickingInfo( - eventScreenCoordinate, - activeViewportId, - viewports - ).then((info) => { - this.publish(info); - }); - } - - private async assembleMultiViewPickingInfo( - eventScreenCoordinate: [number, number], - activeViewportId: string, - viewports: Viewport[] - ): Promise { - return new Promise((resolve, reject) => { - const deck = this._deckGl?.deck; - if (!deck) { - reject("DeckGL not initialized"); - return; - } - const activeViewport = viewports.find( - (el) => el.id === activeViewportId - ); - if (!activeViewport) { - reject("Active viewport not found"); - return; - } - - const activeViewportRelativeScreenCoordinates: [number, number] = [ - eventScreenCoordinate[0] - activeViewport.x, - eventScreenCoordinate[1] - activeViewport.y, - ]; - - const worldCoordinate = activeViewport.unproject( - activeViewportRelativeScreenCoordinates - ); - - const collectedPickingInfo: PickingInfoPerView = {}; - for (const viewport of viewports) { - const [relativeScreenX, relativeScreenY] = - viewport.project(worldCoordinate); - - let pickingInfo: PickingInfo[] = []; - if (this._multiPicking) { - pickingInfo = deck.pickMultipleObjects({ - x: relativeScreenX + viewport.x, - y: relativeScreenY + viewport.y, - unproject3D: true, - }); - } else { - const obj = deck.pickObject({ - x: relativeScreenX + viewport.x, - y: relativeScreenY + viewport.y, - unproject3D: true, - }); - pickingInfo = obj ? [obj] : []; - } - - if (pickingInfo) { - const collectedProperties: PickingInfoProperty[] = []; - for (const info of pickingInfo) { - if ( - !("properties" in info) || - !Array.isArray(info.properties) - ) { - continue; - } - - const properties = info.properties; - - for (const property of properties) { - collectedProperties.push({ - name: property.name, - value: property.value, - color: property.color, - }); - } - } - - collectedPickingInfo[viewport.id] = { - x: worldCoordinate[0], - y: worldCoordinate[1], - properties: collectedProperties, - }; - } else { - collectedPickingInfo[viewport.id] = { - x: null, - y: null, - properties: [], - }; - } - } - - resolve(collectedPickingInfo); - }); - } -} - function ExampleReadoutComponent(props: { viewId: string; pickingInfoPerView: PickingInfoPerView; @@ -270,17 +106,30 @@ function ExampleReadoutComponent(props: { >
X:
- {props.pickingInfoPerView[props.viewId]?.x?.toFixed(3) ?? "-"} + {props.pickingInfoPerView[props.viewId]?.coordinates + ?.at(0) + ?.toFixed(3) ?? "-"}
Y:
- {props.pickingInfoPerView[props.viewId]?.y?.toFixed(3) ?? "-"} + {props.pickingInfoPerView[props.viewId]?.coordinates + ?.at(1) + ?.toFixed(3) ?? "-"}
- {props.pickingInfoPerView[props.viewId]?.properties?.map( - (el, i) => ( - -
{el.name}
-
{el.value.toFixed(3)}
+ {props.pickingInfoPerView[props.viewId]?.layerPickingInfo.map( + (el) => ( + +
{el.layerName}
+ {el.properties.map((prop, i) => ( + +
{prop.name}
+
+ {typeof prop.value === "string" + ? prop.value + : prop.value.toFixed(3)} +
+
+ ))}
) ) ?? ""} @@ -291,51 +140,74 @@ function ExampleReadoutComponent(props: { function MultiViewPickingExample( props: SubsurfaceViewerProps ): React.ReactNode { - const [pickingInfoPerView, setPickingInfoPerView] = - React.useState( - props.views?.viewports.reduce((acc, viewport) => { - acc[viewport.id] = { - x: null, - y: null, - properties: [], - }; - return acc; - }, {} as PickingInfoPerView) ?? {} - ); - const deckGlRef = React.useRef(null); - const assembler = React.useRef(null); - - React.useEffect(function onMountEffect() { - assembler.current = new MultiViewPickingInfoAssembler( - deckGlRef.current - ); - - const unsubscribe = assembler.current.subscribe((info) => { - setPickingInfoPerView(info); + const [mouseHover, setMouseHover] = React.useState(false); + const [cameraPosition, setCameraPosition] = React.useState< + ViewStateType | undefined + >(undefined); + + const { getPickingInfo, activeViewportId, pickingInfoPerView } = + useMultiViewPicking({ + deckGlRef, + multiPicking: true, + pickDepth: 2, }); - return function onUnmountEffect() { - unsubscribe(); - }; - }, []); - function handleMouseEvent(event: MapMouseEvent) { if (event.type === "hover") { - assembler.current?.getMultiViewPickingInfo(event); + getPickingInfo(event); } } + const viewports = props.views?.viewports ?? []; + const layers = props.layers ?? []; + + const { viewports: adjustedViewports, layers: adjustedLayers } = + useMultiViewCursorTracking({ + activeViewportId, + worldCoordinates: + pickingInfoPerView[activeViewportId]?.coordinates ?? null, + viewports, + layers, + crosshairProps: { + color: [255, 255, 255, 255], + sizePx: 32, + visible: mouseHover, + }, + }); + return ( -
+
setMouseHover(true)} + onMouseLeave={() => setMouseHover(false)} + onBlur={() => setMouseHover(false)} + onFocus={() => setMouseHover(true)} + > setCameraPosition(position)} + layers={adjustedLayers} + views={{ + ...props.views, + + viewports: adjustedViewports, + layout: props.views?.layout ?? [1, 2], + }} deckGlRef={deckGlRef} + cameraPosition={cameraPosition} onMouseEvent={handleMouseEvent} coords={{ visible: false, multiPicking: true, }} + scale={{ + visible: true, + cssStyle: { + right: 10, + top: 10, + }, + }} > { // eslint-disable-next-line @typescript-eslint/ban-ts-comment From 9295571306f8277dc838a25e2415c8263d229f5a Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Thu, 19 Dec 2024 15:22:35 +0100 Subject: [PATCH 2/7] fix: remove unused component --- .../subsurface-viewer/src/components/View.tsx | 29 ------------------- 1 file changed, 29 deletions(-) delete mode 100644 typescript/packages/subsurface-viewer/src/components/View.tsx diff --git a/typescript/packages/subsurface-viewer/src/components/View.tsx b/typescript/packages/subsurface-viewer/src/components/View.tsx deleted file mode 100644 index f852d186ee..0000000000 --- a/typescript/packages/subsurface-viewer/src/components/View.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Deck, Viewport, View } from "@deck.gl/core"; -import { DeckGLRef } from "@deck.gl/react"; -import React from "react"; - -export type ViewProps = { - id: string; - deckGlRef: React.RefObject; - children?: React.ReactNode; -}; - -export function ViewContent(props: ViewProps): React.ReactNode { - const deckRef = React.useRef(null); - - React.useEffect( - function onMount() { - if (props.deckGlRef.current) { - deckRef.current = props.deckGlRef.current.deck ?? null; - if (deckRef.current) { - const views = deckRef.current.getViews(); - const view = views.find((v) => v.id === props.id); - if (view) { - deckRef.current.props.onViewStateChange - } - } - } - }, - [props.deckGlRef] - ); -} \ No newline at end of file From b1cedf51d5d28004512bafdb34f5ddddc8e5c04e Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Fri, 20 Dec 2024 12:08:03 +0100 Subject: [PATCH 3/7] fix: Improvements to `mapAnnotation` function and changes to python dir --- .../DashSubsurfaceViewer.tsx | 23 ---- .../components/DashSubsurfaceViewer/index.ts | 1 - .../ReadoutComponent/ReadoutComponent.tsx | 70 ++++++++++ .../src/components/ReadoutComponent/index.ts | 1 + .../SubsurfaceViewer/SubsurfaceViewer.tsx | 123 +++++++----------- python/src/index.ts | 5 +- .../src/DashSubsurfaceViewer.tsx | 9 +- 7 files changed, 129 insertions(+), 103 deletions(-) delete mode 100644 python/src/components/DashSubsurfaceViewer/DashSubsurfaceViewer.tsx delete mode 100644 python/src/components/DashSubsurfaceViewer/index.ts create mode 100644 python/src/components/ReadoutComponent/ReadoutComponent.tsx create mode 100644 python/src/components/ReadoutComponent/index.ts diff --git a/python/src/components/DashSubsurfaceViewer/DashSubsurfaceViewer.tsx b/python/src/components/DashSubsurfaceViewer/DashSubsurfaceViewer.tsx deleted file mode 100644 index fc0ac17d37..0000000000 --- a/python/src/components/DashSubsurfaceViewer/DashSubsurfaceViewer.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from "react"; -import { SubsurfaceViewerProps } from "@webviz/subsurface-viewer"; - -const DashSubsurfaceViewerComponent = React.lazy(() => - import( - /* webpackChunkName: "webviz-subsurface-viewer" */ "@webviz/subsurface-viewer" - ).then((module) => ({ - default: - module.DashSubsurfaceViewer as unknown as React.ComponentType, - })) -); - -const DashSubsurfaceViewer: React.FC = (props) => { - return ( - Loading...
}> - - - ); -}; - -DashSubsurfaceViewer.displayName = "DashSubsurfaceViewer"; - -export default DashSubsurfaceViewer; diff --git a/python/src/components/DashSubsurfaceViewer/index.ts b/python/src/components/DashSubsurfaceViewer/index.ts deleted file mode 100644 index 5c34984510..0000000000 --- a/python/src/components/DashSubsurfaceViewer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./DashSubsurfaceViewer"; diff --git a/python/src/components/ReadoutComponent/ReadoutComponent.tsx b/python/src/components/ReadoutComponent/ReadoutComponent.tsx new file mode 100644 index 0000000000..849a0daf89 --- /dev/null +++ b/python/src/components/ReadoutComponent/ReadoutComponent.tsx @@ -0,0 +1,70 @@ +import React from "react"; + +import { PickingInfoPerView } from "@webviz/subsurface-viewer/src/hooks/useMultiViewPicking"; + +function ReadoutComponent(props: { + viewId: string; + pickingInfoPerView: PickingInfoPerView; +}): React.ReactNode { + return ( +
+
X:
+
+ {roundToSignificant( + props.pickingInfoPerView[props.viewId]?.coordinates?.at(0) + )} +
+
Y:
+
+ {roundToSignificant( + props.pickingInfoPerView[props.viewId]?.coordinates?.at(1) + )} +
+ {props.pickingInfoPerView[props.viewId]?.layerPickingInfo.map( + (el) => ( + +
{el.layerName}
+ {el.properties.map((prop, i) => ( + +
{prop.name}
+
+ {typeof prop.value === "string" + ? prop.value + : roundToSignificant(prop.value)} +
+
+ ))} +
+ ) + ) ?? ""} +
+ ); +} + +function roundToSignificant(num: number | undefined) { + if (num === undefined) { + return "-"; + } + // Returns two significant figures (non-zero) for numbers with an absolute value less + // than 1, and two decimal places for numbers with an absolute value greater + // than 1. + return parseFloat( + num.toExponential(Math.max(1, 2 + Math.log10(Math.abs(num)))) + ); +} + +export default ReadoutComponent; diff --git a/python/src/components/ReadoutComponent/index.ts b/python/src/components/ReadoutComponent/index.ts new file mode 100644 index 0000000000..bcd26badbf --- /dev/null +++ b/python/src/components/ReadoutComponent/index.ts @@ -0,0 +1 @@ +export { default } from "./ReadoutComponent"; \ No newline at end of file diff --git a/python/src/components/SubsurfaceViewer/SubsurfaceViewer.tsx b/python/src/components/SubsurfaceViewer/SubsurfaceViewer.tsx index 3b5ca72d19..d061b647a3 100644 --- a/python/src/components/SubsurfaceViewer/SubsurfaceViewer.tsx +++ b/python/src/components/SubsurfaceViewer/SubsurfaceViewer.tsx @@ -5,10 +5,14 @@ import { ViewStateType, } from "@webviz/subsurface-viewer"; import { DeckGLRef } from "@deck.gl/react"; -import { PickingInfoPerView, useMultiViewPicking } from "@webviz/subsurface-viewer/src/hooks/useMultiViewPicking"; +import { + PickingInfoPerView, + useMultiViewPicking, +} from "@webviz/subsurface-viewer/src/hooks/useMultiViewPicking"; import { useMultiViewCursorTracking } from "@webviz/subsurface-viewer/src/hooks/useMultiViewCursorTracking"; import { isEqual } from "lodash"; import ViewAnnotation from "../ViewAnnotation/ViewAnnotation"; +import ReadoutComponent from "../ReadoutComponent"; const SubsurfaceViewerComponent = React.lazy(() => import( @@ -102,18 +106,47 @@ function MultiViewSubsurfaceViewer( visible: mouseHover, }, }); - - const children = React.Children.toArray(props.children); - for (const viewport of adjustedViewports) { - children.push( - - - - ); - } + + const foundViewAnnotations: string[] = []; + const children = React.Children.map(props.children, (child) => { + // Child wrapped in DashWrapper + if ( + React.isValidElement(child) && + typeof child.props === "object" && + Object.keys(child.props).includes("_dashprivate_layout") && + child.props._dashprivate_layout.type === "ViewAnnotation" + ) { + const id = child.props._dashprivate_layout.props.id; + const readout = adjustedViewports.find( + (viewport) => viewport.id === id + ); + if (!readout) { + return child; + } + foundViewAnnotations.push(id); + const newChild = React.cloneElement(child, { + // @ts-expect-error - this is proven to be a valid prop in Dash components + _dashprivate_layout: { + ...child.props._dashprivate_layout, + props: { + ...child.props._dashprivate_layout.props, + children: [ + ...child.props._dashprivate_layout.props.children, + { + type: "ReadoutComponent", + props: { + viewId: id, + pickingInfoPerView, + }, + namespace: "webviz_subsurface_components", + }, + ], + }, + }, + }); + return newChild; + } + }); return (
-
X:
-
- {roundToSignificant(props.pickingInfoPerView[props.viewId]?.coordinates - ?.at(0))} -
-
Y:
-
- {roundToSignificant(props.pickingInfoPerView[props.viewId]?.coordinates - ?.at(1))} -
- {props.pickingInfoPerView[props.viewId]?.layerPickingInfo.map( - (el) => ( - -
{el.layerName}
- {el.properties.map((prop, i) => ( - -
{prop.name}
-
- {typeof prop.value === "string" - ? prop.value - : roundToSignificant(prop.value)} -
-
- ))} -
- ) - ) ?? ""} -
- ); -} - -const roundToSignificant = function (num: number | undefined) { - if (num === undefined) { - return "-"; - } - // Returns two significant figures (non-zero) for numbers with an absolute value less - // than 1, and two decimal places for numbers with an absolute value greater - // than 1. - return parseFloat( - num.toExponential(Math.max(1, 2 + Math.log10(Math.abs(num)))) - ); -}; - SubsurfaceViewer.displayName = "SubsurfaceViewer"; export default SubsurfaceViewer; diff --git a/python/src/index.ts b/python/src/index.ts index 0fe54b80a4..2783a7becf 100644 --- a/python/src/index.ts +++ b/python/src/index.ts @@ -1,4 +1,3 @@ -import DashSubsurfaceViewer from "./components/DashSubsurfaceViewer"; import SubsurfaceViewer from "./components/SubsurfaceViewer"; import { GroupTree } from "./components/GroupTree"; import HistoryMatch from "./components/HistoryMatch"; @@ -10,6 +9,7 @@ import VectorSelector from "./components/VectorSelector"; import { WellCompletions } from "./components/WellCompletions"; import { VectorCalculator } from "./components/VectorCalculator"; import WellLogViewer from "./components/WellLogViewer"; +import ReadoutComponent from "./components/ReadoutComponent"; import SyncLogViewer from "./components/WellLogViewer"; import WebVizContinuousLegend from "./components/ColorLegends/WebVizContinuousLegend"; import WebVizDiscreteLegend from "./components/ColorLegends/WebVizDiscreteLegend"; @@ -30,12 +30,13 @@ export { * @deprecated Use the {@link SubsurfaceViewer} component instead. */ SubsurfaceViewer as DeckGLMap, // For backwards compatibility - DashSubsurfaceViewer, + SubsurfaceViewer as DashSubsurfaceViewer, // For backwards compatibility VectorSelector, WellCompletions, VectorCalculator, GroupTree, WellLogViewer, + ReadoutComponent, SyncLogViewer, WebVizContinuousLegend, WebVizDiscreteLegend, diff --git a/typescript/packages/subsurface-viewer/src/DashSubsurfaceViewer.tsx b/typescript/packages/subsurface-viewer/src/DashSubsurfaceViewer.tsx index b105fe0d1f..511d433f2c 100644 --- a/typescript/packages/subsurface-viewer/src/DashSubsurfaceViewer.tsx +++ b/typescript/packages/subsurface-viewer/src/DashSubsurfaceViewer.tsx @@ -2,10 +2,17 @@ import React from "react"; import type { SubsurfaceViewerProps } from "./SubsurfaceViewer"; import SubsurfaceViewer from "./SubsurfaceViewer"; import { View } from "@deck.gl/core"; +import { ViewAnnotation } from "./components/ViewAnnotation"; function mapAnnotation(annotationContainers: React.ReactNode) { return React.Children.map(annotationContainers, (annotationContainer) => { - const viewId = (annotationContainer as React.ReactElement).key; + let viewId = (annotationContainer as React.ReactElement).props.id; + if (React.isValidElement(annotationContainer) && (annotationContainer.type === ViewAnnotation || (annotationContainer.props instanceof Object && Object.keys(annotationContainer.props).includes("_dashprivate_layout")))) { + viewId = annotationContainer.props._dashprivate_layout.props.id; + } + if (!viewId) { + return null; + } return ( // @ts-expect-error This is proven to work in JavaScript From b4ac4aa6a6efa9d49902a79b6f07df11764cfc69 Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Fri, 20 Dec 2024 12:10:27 +0100 Subject: [PATCH 4/7] Adjusted bottom margin of readout box --- python/src/components/ReadoutComponent/ReadoutComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/src/components/ReadoutComponent/ReadoutComponent.tsx b/python/src/components/ReadoutComponent/ReadoutComponent.tsx index 849a0daf89..b5abcc0554 100644 --- a/python/src/components/ReadoutComponent/ReadoutComponent.tsx +++ b/python/src/components/ReadoutComponent/ReadoutComponent.tsx @@ -10,7 +10,7 @@ function ReadoutComponent(props: {
Date: Fri, 20 Dec 2024 12:33:46 +0100 Subject: [PATCH 5/7] fix: linting issues --- python/src/components/ReadoutComponent/index.ts | 2 +- .../src/components/SubsurfaceViewer/SubsurfaceViewer.tsx | 7 +------ python/src/components/WellLogViewer/WellLogViewer.tsx | 5 ++--- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/python/src/components/ReadoutComponent/index.ts b/python/src/components/ReadoutComponent/index.ts index bcd26badbf..edbc7ca133 100644 --- a/python/src/components/ReadoutComponent/index.ts +++ b/python/src/components/ReadoutComponent/index.ts @@ -1 +1 @@ -export { default } from "./ReadoutComponent"; \ No newline at end of file +export { default } from "./ReadoutComponent"; diff --git a/python/src/components/SubsurfaceViewer/SubsurfaceViewer.tsx b/python/src/components/SubsurfaceViewer/SubsurfaceViewer.tsx index d061b647a3..e128106b80 100644 --- a/python/src/components/SubsurfaceViewer/SubsurfaceViewer.tsx +++ b/python/src/components/SubsurfaceViewer/SubsurfaceViewer.tsx @@ -5,14 +5,9 @@ import { ViewStateType, } from "@webviz/subsurface-viewer"; import { DeckGLRef } from "@deck.gl/react"; -import { - PickingInfoPerView, - useMultiViewPicking, -} from "@webviz/subsurface-viewer/src/hooks/useMultiViewPicking"; +import { useMultiViewPicking } from "@webviz/subsurface-viewer/src/hooks/useMultiViewPicking"; import { useMultiViewCursorTracking } from "@webviz/subsurface-viewer/src/hooks/useMultiViewCursorTracking"; import { isEqual } from "lodash"; -import ViewAnnotation from "../ViewAnnotation/ViewAnnotation"; -import ReadoutComponent from "../ReadoutComponent"; const SubsurfaceViewerComponent = React.lazy(() => import( diff --git a/python/src/components/WellLogViewer/WellLogViewer.tsx b/python/src/components/WellLogViewer/WellLogViewer.tsx index 05de630744..66a6846b37 100644 --- a/python/src/components/WellLogViewer/WellLogViewer.tsx +++ b/python/src/components/WellLogViewer/WellLogViewer.tsx @@ -9,7 +9,6 @@ const WellLogViewerComponent = React.lazy(() => })) ); - // react-docgen / dash-generate-components/extract-meta.js does not properly parse // the imported WellLogViewerProps. Hence, we have to recreate them here. type WellLogViewOptions = { @@ -51,7 +50,7 @@ type WellLogViewerProps = { template: object; /** Prop containing color function/table array */ - colorMapFunctions: any; + colorMapFunctions: unknown; /** Orientation of the track plots on the screen. Default is false */ horizontal?: boolean; @@ -63,7 +62,7 @@ type WellLogViewerProps = { selection?: number[]; /** Well picks data */ - wellpick?: any; + wellpick?: unknown; /** Primary axis id: " md", "tvd", "time"... */ primaryAxis?: string; From 7000e7a38038dae6e0ea660ffe39d03b20f5e004 Mon Sep 17 00:00:00 2001 From: Ruben Thoms Date: Fri, 20 Dec 2024 12:43:49 +0100 Subject: [PATCH 6/7] fix: linting --- .../src/DashSubsurfaceViewer.tsx | 9 ++- .../MultiViewPickingInfoAssembler.ts | 71 ++++++++++++++----- .../src/layers/crosshair/crosshairLayer.ts | 4 +- .../examples/MultiViewExamples.stories.tsx | 5 +- 4 files changed, 68 insertions(+), 21 deletions(-) diff --git a/typescript/packages/subsurface-viewer/src/DashSubsurfaceViewer.tsx b/typescript/packages/subsurface-viewer/src/DashSubsurfaceViewer.tsx index 511d433f2c..e0a145e72a 100644 --- a/typescript/packages/subsurface-viewer/src/DashSubsurfaceViewer.tsx +++ b/typescript/packages/subsurface-viewer/src/DashSubsurfaceViewer.tsx @@ -7,7 +7,14 @@ import { ViewAnnotation } from "./components/ViewAnnotation"; function mapAnnotation(annotationContainers: React.ReactNode) { return React.Children.map(annotationContainers, (annotationContainer) => { let viewId = (annotationContainer as React.ReactElement).props.id; - if (React.isValidElement(annotationContainer) && (annotationContainer.type === ViewAnnotation || (annotationContainer.props instanceof Object && Object.keys(annotationContainer.props).includes("_dashprivate_layout")))) { + if ( + React.isValidElement(annotationContainer) && + (annotationContainer.type === ViewAnnotation || + (annotationContainer.props instanceof Object && + Object.keys(annotationContainer.props).includes( + "_dashprivate_layout" + ))) + ) { viewId = annotationContainer.props._dashprivate_layout.props.id; } if (!viewId) { diff --git a/typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/MultiViewPickingInfoAssembler.ts b/typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/MultiViewPickingInfoAssembler.ts index 1b7757f876..3abb9248a7 100644 --- a/typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/MultiViewPickingInfoAssembler.ts +++ b/typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/MultiViewPickingInfoAssembler.ts @@ -1,6 +1,10 @@ import type { PickingInfo, Viewport } from "@deck.gl/core"; -import { DeckGLRef } from "@deck.gl/react"; -import { ExtendedLayerProps, MapMouseEvent, PropertyDataType } from "../../"; +import type { DeckGLRef } from "@deck.gl/react"; +import type { + ExtendedLayerProps, + MapMouseEvent, + PropertyDataType, +} from "../../"; export type LayerPickingInfo = { layerId: string; @@ -16,12 +20,16 @@ export type PickingInfoPerView = Record< } >; -function hasPropertiesArray(obj: any): obj is { properties: PropertyDataType[] } { +function hasPropertiesArray( + obj: any +): obj is { properties: PropertyDataType[] } { return obj && Array.isArray(obj.properties); } function hasSingleProperty(obj: any): obj is { propertyValue: number } { - return obj && "propertyValue" in obj && typeof obj.propertyValue === "number"; + return ( + obj && "propertyValue" in obj && typeof obj.propertyValue === "number" + ); } export type MultiViewPickingInfoAssemblerOptions = { @@ -36,11 +44,15 @@ export interface MultiViewPickingInfoAssemblerSubscriberCallback { export class MultiViewPickingInfoAssembler { private _deckGl: DeckGLRef | null = null; private _options: MultiViewPickingInfoAssemblerOptions; - private _subscribers: Set = new Set(); + private _subscribers: Set = + new Set(); constructor( deckGL: DeckGLRef | null, - options: MultiViewPickingInfoAssemblerOptions = { multiPicking: false, pickDepth: 1 } + options: MultiViewPickingInfoAssemblerOptions = { + multiPicking: false, + pickDepth: 1, + } ) { this._deckGl = deckGL; this._options = options; @@ -50,7 +62,9 @@ export class MultiViewPickingInfoAssembler { this._deckGl = deckGL; } - subscribe(callback: MultiViewPickingInfoAssemblerSubscriberCallback): () => void { + subscribe( + callback: MultiViewPickingInfoAssemblerSubscriberCallback + ): () => void { this._subscribers.add(callback); return () => { @@ -84,9 +98,16 @@ export class MultiViewPickingInfoAssembler { return; } - const eventScreenCoordinate: [number, number] = [hoverEvent.infos[0].x, hoverEvent.infos[0].y]; + const eventScreenCoordinate: [number, number] = [ + hoverEvent.infos[0].x, + hoverEvent.infos[0].y, + ]; - this.assembleMultiViewPickingInfo(eventScreenCoordinate, activeViewportId, viewports).then((info) => { + this.assembleMultiViewPickingInfo( + eventScreenCoordinate, + activeViewportId, + viewports + ).then((info) => { this.publish(info, activeViewportId); }); } @@ -102,7 +123,9 @@ export class MultiViewPickingInfoAssembler { reject("DeckGL not initialized"); return; } - const activeViewport = viewports.find((el) => el.id === activeViewportId); + const activeViewport = viewports.find( + (el) => el.id === activeViewportId + ); if (!activeViewport) { reject("Active viewport not found"); return; @@ -113,11 +136,14 @@ export class MultiViewPickingInfoAssembler { eventScreenCoordinate[1] - activeViewport.y, ]; - const worldCoordinate = activeViewport.unproject(activeViewportRelativeScreenCoordinates); + const worldCoordinate = activeViewport.unproject( + activeViewportRelativeScreenCoordinates + ); const collectedPickingInfo: PickingInfoPerView = {}; for (const viewport of viewports) { - const [relativeScreenX, relativeScreenY] = viewport.project(worldCoordinate); + const [relativeScreenX, relativeScreenY] = + viewport.project(worldCoordinate); let pickingInfo: PickingInfo[] = []; if (this._options.multiPicking) { @@ -150,14 +176,22 @@ export class MultiViewPickingInfoAssembler { continue; } - if (collectedLayerPickingInfo.find((el) => el.layerId === info.layer?.id)) { + if ( + collectedLayerPickingInfo.find( + (el) => el.layerId === info.layer?.id + ) + ) { continue; } const layerId = info.layer.id; - const layerName = (info.layer.props as unknown as ExtendedLayerProps).name; + const layerName = ( + info.layer.props as unknown as ExtendedLayerProps + ).name; - let layerPickingInfo = collectedLayerPickingInfo.find((el) => el.layerId === layerId); + let layerPickingInfo = collectedLayerPickingInfo.find( + (el) => el.layerId === layerId + ); if (!layerPickingInfo) { collectedLayerPickingInfo.push({ @@ -165,7 +199,10 @@ export class MultiViewPickingInfoAssembler { layerName, properties: [], }); - layerPickingInfo = collectedLayerPickingInfo[collectedLayerPickingInfo.length - 1]; + layerPickingInfo = + collectedLayerPickingInfo[ + collectedLayerPickingInfo.length - 1 + ]; } if (hasOneProperty) { @@ -205,4 +242,4 @@ export class MultiViewPickingInfoAssembler { resolve(collectedPickingInfo); }); } -} \ No newline at end of file +} diff --git a/typescript/packages/subsurface-viewer/src/layers/crosshair/crosshairLayer.ts b/typescript/packages/subsurface-viewer/src/layers/crosshair/crosshairLayer.ts index 22bde667a6..685ec415ab 100644 --- a/typescript/packages/subsurface-viewer/src/layers/crosshair/crosshairLayer.ts +++ b/typescript/packages/subsurface-viewer/src/layers/crosshair/crosshairLayer.ts @@ -1,6 +1,8 @@ -import { CompositeLayer, DefaultProps } from "@deck.gl/core"; +import { CompositeLayer } from "@deck.gl/core"; import { IconLayer } from "@deck.gl/layers"; +import type { DefaultProps } from "@deck.gl/core"; + function makeCrossHairSvg(color: [number, number, number, number]): string { return ` Date: Fri, 20 Dec 2024 12:47:39 +0100 Subject: [PATCH 7/7] fix: linting --- .../MultiViewPickingInfoAssembler.ts | 2 ++ .../src/hooks/useMultiViewPicking/index.ts | 12 +++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/MultiViewPickingInfoAssembler.ts b/typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/MultiViewPickingInfoAssembler.ts index 3abb9248a7..47fba3aee7 100644 --- a/typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/MultiViewPickingInfoAssembler.ts +++ b/typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/MultiViewPickingInfoAssembler.ts @@ -21,11 +21,13 @@ export type PickingInfoPerView = Record< >; function hasPropertiesArray( + // eslint-disable-next-line @typescript-eslint/no-explicit-any obj: any ): obj is { properties: PropertyDataType[] } { return obj && Array.isArray(obj.properties); } +// eslint-disable-next-line @typescript-eslint/no-explicit-any function hasSingleProperty(obj: any): obj is { propertyValue: number } { return ( obj && "propertyValue" in obj && typeof obj.propertyValue === "number" diff --git a/typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/index.ts b/typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/index.ts index 7c9661b5b2..c82087a4e3 100644 --- a/typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/index.ts +++ b/typescript/packages/subsurface-viewer/src/hooks/useMultiViewPicking/index.ts @@ -1,3 +1,9 @@ -export { useMultiViewPicking } from './useMultiViewPicking'; -export type { UseMultiViewPickingProps, UseMultiViewPickingReturnType } from './useMultiViewPicking'; -export type { PickingInfoPerView, LayerPickingInfo} from './MultiViewPickingInfoAssembler'; \ No newline at end of file +export { useMultiViewPicking } from "./useMultiViewPicking"; +export type { + UseMultiViewPickingProps, + UseMultiViewPickingReturnType, +} from "./useMultiViewPicking"; +export type { + PickingInfoPerView, + LayerPickingInfo, +} from "./MultiViewPickingInfoAssembler";