From f71fdc0bd36f13ec12d29406acf804a1d334d1d8 Mon Sep 17 00:00:00 2001 From: Tobias O Date: Tue, 21 Jan 2025 12:11:05 +0100 Subject: [PATCH 1/5] refactor: decouple Molstar component calls from svelte --- .../StructureViewer/MolstarWrapper.ts | 179 ++++++++++++++++++ .../StructureViewer/StructureViewer.svelte | 175 ++--------------- 2 files changed, 195 insertions(+), 159 deletions(-) create mode 100644 frontend/src/lib/components/StructureViewer/MolstarWrapper.ts diff --git a/frontend/src/lib/components/StructureViewer/MolstarWrapper.ts b/frontend/src/lib/components/StructureViewer/MolstarWrapper.ts new file mode 100644 index 0000000..4e16c3e --- /dev/null +++ b/frontend/src/lib/components/StructureViewer/MolstarWrapper.ts @@ -0,0 +1,179 @@ +// Copyright 2025 Tobias Olenyi. +// SPDX-License-Identifier: Apache-2.0 +import type { + HighlightState, + SelectionState, + StructureSelectionQuery, +} from "$lib/stores/StructureMarksStore"; + +import type { RGB } from "$lib/utils"; + +export class MolstarWrapper { + private viewer: any; + private ready: boolean = false; + private readyCallbacks: (() => void)[] = []; + + constructor(viewerElement: any) { + this.viewer = viewerElement; + this.initializeWhenReady(); + } + + private initializeWhenReady() { + if (this.viewer?.viewerInstance) { + this.viewer.viewerInstance.events.loadComplete.subscribe( + (success: boolean) => { + if (success) { + this.ready = true; + this.readyCallbacks.forEach((cb) => cb()); + this.readyCallbacks = []; + } + }, + ); + } + } + + async whenReady(): Promise { + if (this.ready) return Promise.resolve(); + return new Promise((resolve) => this.readyCallbacks.push(resolve)); + } + + get plugin() { + return this.viewer?.viewerInstance?.plugin; + } + + get canvas() { + return this.viewer?.viewerInstance?.canvas; + } + + getBackgroundColor(): RGB { + const parentElement = this.viewer?.parentElement; + let bgColor = "rgb(255, 255, 255)"; + + if (parentElement) { + const computedStyle = getComputedStyle(parentElement); + bgColor = computedStyle.backgroundColor; + + if ( + !bgColor || + bgColor === "transparent" || + bgColor === "rgba(0, 0, 0, 0)" + ) { + bgColor = getComputedStyle(document.body).backgroundColor; + } + } + + const rgbValues = bgColor.match(/\d+/g)?.map(Number) ?? [255, 255, 255]; + return { + r: rgbValues[0], + g: rgbValues[1], + b: rgbValues[2], + }; + } + + async updateBackground() { + await this.whenReady(); + if (this.canvas) { + this.canvas.setBgColor(this.getBackgroundColor()); + } + } + + async select( + residues: StructureSelectionQuery[], + color?: RGB | null, + nonSelectedColor?: RGB | null, + structureId?: string, + structureNumber?: number, + keepColors?: boolean, + keepRepresentations?: boolean, + ) { + await this.whenReady(); + await this.viewer.viewerInstance.visual.select({ + data: residues, + color, + nonSelectedColor, + structureId, + structureNumber, + keepColors, + keepRepresentations, + }); + await this.viewer.viewerInstance.visual.tooltips({ data: residues }); + } + + async highlight( + residues: StructureSelectionQuery[], + color?: RGB | null, + focus?: boolean, + structureId?: string, + structureNumber?: number, + ) { + await this.whenReady(); + return await this.viewer.viewerInstance.visual.highlight({ + data: residues, + color, + focus, + structureId, + structureNumber, + }); + } + + async clearHighlight() { + await this.whenReady(); + return await this.viewer.viewerInstance.visual.clearHighlight(); + } + + async clearSelection( + keepColors: boolean = false, + keepRepresentations: boolean = true, + ) { + await this.whenReady(); + await this.viewer.viewerInstance.visual.clearSelection({ + keepColors, + keepRepresentations, + }); + await this.viewer.viewerInstance.visual.clearTooltips(); + } + + async updateSelectionState(state: SelectionState | null) { + await this.whenReady(); + if (!state) { + await this.clearSelection(); + return; + } + + const { + residues, + color, + nonSelectedColor, + structureId, + structureNumber, + keepColors, + keepRepresentations, + } = state; + + await this.select( + residues, + color, + nonSelectedColor, + structureId, + structureNumber, + keepColors, + keepRepresentations, + ); + } + + async updateHighlightState(state: HighlightState | null) { + await this.whenReady(); + if (!state) { + await this.clearHighlight(); + return; + } + + const { residues, color, focus, structureId, structureNumber } = state; + await this.highlight(residues, color, focus, structureId, structureNumber); + } + + async applyConfidenceVisualization() { + await this.whenReady(); + //await this.applyTransparency(70, 0.3); + } +} diff --git a/frontend/src/lib/components/StructureViewer/StructureViewer.svelte b/frontend/src/lib/components/StructureViewer/StructureViewer.svelte index e342e4d..fe70bf7 100644 --- a/frontend/src/lib/components/StructureViewer/StructureViewer.svelte +++ b/frontend/src/lib/components/StructureViewer/StructureViewer.svelte @@ -1,12 +1,10 @@ From a0b8767a05574e1151d0201aa6fbb19adf07e264 Mon Sep 17 00:00:00 2001 From: Tobias O Date: Tue, 21 Jan 2025 12:21:01 +0100 Subject: [PATCH 2/5] fix: add observer for pageload event --- .../StructureViewer/MolstarWrapper.ts | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/components/StructureViewer/MolstarWrapper.ts b/frontend/src/lib/components/StructureViewer/MolstarWrapper.ts index 4e16c3e..4265c81 100644 --- a/frontend/src/lib/components/StructureViewer/MolstarWrapper.ts +++ b/frontend/src/lib/components/StructureViewer/MolstarWrapper.ts @@ -18,17 +18,40 @@ export class MolstarWrapper { this.initializeWhenReady(); } + private setReady() { + this.ready = true; + this.readyCallbacks.forEach((cb) => cb()); + this.readyCallbacks = []; + } + private initializeWhenReady() { - if (this.viewer?.viewerInstance) { + const setupLoadCompleteListener = () => { this.viewer.viewerInstance.events.loadComplete.subscribe( (success: boolean) => { if (success) { - this.ready = true; - this.readyCallbacks.forEach((cb) => cb()); - this.readyCallbacks = []; + this.setReady(); } }, ); + }; + + if (this.viewer?.viewerInstance) { + setupLoadCompleteListener(); + } else { + // Create MutationObserver to watch for viewerInstance + const observer = new MutationObserver(() => { + if (this.viewer?.viewerInstance) { + setupLoadCompleteListener(); + observer.disconnect(); + } + }); + + // Observe the viewer element for changes + observer.observe(this.viewer, { + subtree: true, + childList: true, + attributes: true, + }); } } @@ -139,7 +162,6 @@ export class MolstarWrapper { await this.clearSelection(); return; } - const { residues, color, From d06c998df50d8d0859ce30a9c27934063cf8d0e5 Mon Sep 17 00:00:00 2001 From: Tobias O Date: Thu, 23 Jan 2025 10:30:55 +0100 Subject: [PATCH 3/5] feat: implement non-working pdbe transparency --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 367 +++++++++++++++++- .../StructureViewer/MolstarWrapper.ts | 180 ++++++++- .../StructureViewer/StructureViewer.svelte | 1 - 4 files changed, 546 insertions(+), 3 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 59f1a40..46bf362 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -54,6 +54,7 @@ "@tanstack/svelte-query": "^5.61.5", "axios": "^1.7.8", "iconify-icon": "^2.1.0", + "molstar": "^4.10.0", "pdbe-molstar": "^3.3.2" } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index d51f921..b26fc0e 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: iconify-icon: specifier: ^2.1.0 version: 2.1.0 + molstar: + specifier: ^4.10.0 + version: 4.10.0(@types/react@18.3.9)(fp-ts@2.16.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) pdbe-molstar: specifier: ^3.3.2 version: 3.3.2(@types/react@18.3.9)(fp-ts@2.16.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -938,9 +941,15 @@ packages: '@types/express-serve-static-core@4.19.6': resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} + '@types/express-serve-static-core@5.0.5': + resolution: {integrity: sha512-GLZPrd9ckqEBFMcVM/qRFAP0Hg3qiVEojgEFsx/N/zKXsBzbGF6z5FBDpZ0+Xhp1xr+qRZYjfGr1cWHB9oFHSA==} + '@types/express@4.17.21': resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + '@types/express@5.0.0': + resolution: {integrity: sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -965,6 +974,9 @@ packages: '@types/node@18.19.67': resolution: {integrity: sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==} + '@types/node@18.19.71': + resolution: {integrity: sha512-evXpcgtZm8FY4jqBSN8+DmOTcVkkvTmAayeo4Wf3m1xAruyVGzGuDh/Fb/WWX2yLItUiho42ozyJjB0dw//Tkw==} + '@types/node@22.10.1': resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==} @@ -1010,6 +1022,10 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-walk@8.3.4: resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} @@ -1063,6 +1079,9 @@ packages: array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + array-flatten@3.0.0: + resolution: {integrity: sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==} + array.prototype.reduce@1.0.7: resolution: {integrity: sha512-mzmiUCVwtiD4lgxYP8g7IYy8El8p2CSMePvIbTS7gchKir/L1fgJrk0yDKmAX6mnRQFKNADYIk8nNlTris5H1Q==} engines: {node: '>= 0.4'} @@ -1124,6 +1143,10 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.0.2: + resolution: {integrity: sha512-SNMk0OONlQ01uk8EPeiBvTW7W4ovpL5b1O3t1sjpPgfxOQ6BqQJ6XjxinDPR79Z6HdcD5zBBwr5ssiTlgdNztQ==} + engines: {node: '>=18'} + brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} @@ -1258,6 +1281,10 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -1268,6 +1295,10 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.6.0: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} @@ -1469,6 +1500,23 @@ packages: supports-color: optional: true + debug@3.1.0: + resolution: {integrity: sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.6: + resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.3.7: resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} engines: {node: '>=6.0'} @@ -1652,6 +1700,10 @@ packages: resolution: {integrity: sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==} engines: {node: '>= 0.10.0'} + express@5.0.1: + resolution: {integrity: sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==} + engines: {node: '>= 18'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -1681,6 +1733,10 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} + finalhandler@2.0.0: + resolution: {integrity: sha512-MX6Zo2adDViYh+GcxxB1dpO43eypOGUOL12rLCOTMQv/DfIbpSJUy4oQIIZhVZkH9e+bZWKMon0XHFEju16tkQ==} + engines: {node: '>= 0.8'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1719,6 +1775,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -1847,6 +1907,10 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.5.2: + resolution: {integrity: sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==} + engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -1860,6 +1924,9 @@ packages: immutable@4.3.7: resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} + immutable@5.0.3: + resolution: {integrity: sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==} + import-meta-resolve@4.1.0: resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} @@ -1885,6 +1952,11 @@ packages: peerDependencies: fp-ts: ^2.5.0 + io-ts@2.2.22: + resolution: {integrity: sha512-FHCCztTkHoV9mdBsHpocLpdTAfh956ZQcIkWQxxS0U5HT53vtrcuYdQneEJKH6xILaLNzXVl2Cvwtoy8XNN0AA==} + peerDependencies: + fp-ts: ^2.5.0 + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -1976,6 +2048,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} @@ -2146,9 +2221,17 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2240,6 +2323,10 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.0: + resolution: {integrity: sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==} + engines: {node: '>= 0.6'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -2281,6 +2368,29 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + molstar@4.10.0: + resolution: {integrity: sha512-sUQhPb74xsqdjmN2Y8vVo0DOwmv3njsiIhZBI4JHJfT//+io/d8KGdPUKJ/MCKXKvfdcumkOJGJWBD2Bfe3D/Q==} + hasBin: true + peerDependencies: + '@google-cloud/storage': ^7.14.0 + canvas: ^2.11.2 + gl: ^6.0.2 + jpeg-js: ^0.4.4 + pngjs: ^6.0.0 + react: '>=16.14.0' + react-dom: '>=16.14.0' + peerDependenciesMeta: + '@google-cloud/storage': + optional: true + canvas: + optional: true + gl: + optional: true + jpeg-js: + optional: true + pngjs: + optional: true + molstar@4.8.0: resolution: {integrity: sha512-RX2zqSJWN4tyyW0r95VcFzKqU3i+muwFxpfxq/tuqOh3zbSgh1vXokJefmyKo7lsaie1odXwQrgR4Wo8bEu+Gw==} hasBin: true @@ -2315,6 +2425,9 @@ packages: ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2338,6 +2451,10 @@ packages: resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + node-fetch@2.6.13: resolution: {integrity: sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==} engines: {node: 4.x || >=6.0.0} @@ -2406,6 +2523,9 @@ packages: resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} engines: {node: '>= 0.8'} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -2458,6 +2578,10 @@ packages: path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.2.0: + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} @@ -2672,6 +2796,10 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -2756,6 +2884,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.0.0: + resolution: {integrity: sha512-dIM5zVoG8xhC6rnSN8uoAgFARwTE7BQs8YwHEvK0VCmfxQXMaOuA1uiR1IPwsW7JyK5iTt7Od/TC9StasS2NPQ==} + engines: {node: '>= 0.10'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2804,10 +2936,18 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + send@1.1.0: + resolution: {integrity: sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==} + engines: {node: '>= 18'} + serve-static@1.16.2: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + serve-static@2.1.0: + resolution: {integrity: sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==} + engines: {node: '>= 18'} + set-cookie-parser@2.7.1: resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} @@ -2994,6 +3134,10 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + type-is@2.0.0: + resolution: {integrity: sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==} + engines: {node: '>= 0.6'} + typed-array-buffer@1.0.2: resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} engines: {node: '>= 0.4'} @@ -3197,6 +3341,9 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -3933,7 +4080,7 @@ snapshots: '@types/compression@1.7.5': dependencies: - '@types/express': 4.17.21 + '@types/express': 5.0.0 '@types/connect@3.4.38': dependencies: @@ -3958,6 +4105,13 @@ snapshots: '@types/range-parser': 1.2.7 '@types/send': 0.17.4 + '@types/express-serve-static-core@5.0.5': + dependencies: + '@types/node': 22.10.1 + '@types/qs': 6.9.17 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + '@types/express@4.17.21': dependencies: '@types/body-parser': 1.19.5 @@ -3965,6 +4119,13 @@ snapshots: '@types/qs': 6.9.17 '@types/serve-static': 1.15.7 + '@types/express@5.0.0': + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 5.0.5 + '@types/qs': 6.9.17 + '@types/serve-static': 1.15.7 + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -3992,6 +4153,10 @@ snapshots: dependencies: undici-types: 5.26.5 + '@types/node@18.19.71': + dependencies: + undici-types: 5.26.5 + '@types/node@22.10.1': dependencies: undici-types: 6.20.0 @@ -4037,6 +4202,11 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.0 + negotiator: 1.0.0 + acorn-walk@8.3.4: dependencies: acorn: 8.14.0 @@ -4079,6 +4249,8 @@ snapshots: array-flatten@1.1.1: {} + array-flatten@3.0.0: {} + array.prototype.reduce@1.0.7: dependencies: call-bind: 1.0.7 @@ -4167,6 +4339,21 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.0.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 3.1.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.5.2 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 3.0.0 + type-is: 1.6.18 + transitivePeerDependencies: + - supports-color + brace-expansion@2.0.1: dependencies: balanced-match: 1.0.2 @@ -4339,12 +4526,18 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + content-type@1.0.5: {} convert-source-map@2.0.0: {} cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} + cookie@0.6.0: {} cookie@0.7.1: {} @@ -4588,6 +4781,14 @@ snapshots: dependencies: ms: 2.0.0 + debug@3.1.0: + dependencies: + ms: 2.0.0 + + debug@4.3.6: + dependencies: + ms: 2.1.2 + debug@4.3.7: dependencies: ms: 2.1.3 @@ -4866,6 +5067,43 @@ snapshots: transitivePeerDependencies: - supports-color + express@5.0.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.0.2 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.2.2 + debug: 4.3.6 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.0.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + methods: 1.1.2 + mime-types: 3.0.0 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + router: 2.0.0 + safe-buffer: 5.2.1 + send: 1.1.0 + serve-static: 2.1.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 2.0.0 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + extend@3.0.2: {} fast-glob@3.3.2: @@ -4904,6 +5142,18 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.0.0: + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -4934,6 +5184,8 @@ snapshots: fresh@0.5.2: {} + fresh@2.0.0: {} + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -5097,6 +5349,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.5.2: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -5107,6 +5363,8 @@ snapshots: immutable@4.3.7: {} + immutable@5.0.3: {} + import-meta-resolve@4.1.0: {} inherits@2.0.4: {} @@ -5130,6 +5388,10 @@ snapshots: dependencies: fp-ts: 2.16.9 + io-ts@2.2.22(fp-ts@2.16.9): + dependencies: + fp-ts: 2.16.9 + ipaddr.js@1.9.1: {} is-alphabetical@2.0.1: {} @@ -5207,6 +5469,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-promise@4.0.0: {} + is-reference@3.0.3: dependencies: '@types/estree': 1.0.6 @@ -5432,8 +5696,12 @@ snapshots: media-typer@0.3.0: {} + media-typer@1.1.0: {} + merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} + merge2@1.4.1: {} methods@1.1.2: {} @@ -5589,6 +5857,10 @@ snapshots: dependencies: mime-db: 1.52.0 + mime-types@3.0.0: + dependencies: + mime-db: 1.53.0 + mime@1.6.0: {} mime@3.0.0: {} @@ -5630,6 +5902,38 @@ snapshots: minipass@7.1.2: {} + molstar@4.10.0(@types/react@18.3.9)(fp-ts@2.16.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@types/argparse': 2.0.17 + '@types/benchmark': 2.1.5 + '@types/compression': 1.7.5 + '@types/express': 5.0.0 + '@types/node': 18.19.71 + '@types/node-fetch': 2.6.12 + '@types/swagger-ui-dist': 3.30.5 + argparse: 2.0.1 + compression: 1.7.5 + cors: 2.8.5 + express: 5.0.1 + h264-mp4-encoder: 1.0.12 + immer: 10.1.1 + immutable: 5.0.3 + io-ts: 2.2.22(fp-ts@2.16.9) + node-fetch: 2.7.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-markdown: 9.0.1(@types/react@18.3.9)(react@18.3.1) + rxjs: 7.8.1 + swagger-ui-dist: 5.18.2 + tslib: 2.8.1 + util.promisify: 1.1.2 + xhr2: 0.2.1 + transitivePeerDependencies: + - '@types/react' + - encoding + - fp-ts + - supports-color + molstar@4.8.0(@types/react@18.3.9)(fp-ts@2.16.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@types/argparse': 2.0.17 @@ -5671,6 +5975,8 @@ snapshots: ms@2.0.0: {} + ms@2.1.2: {} + ms@2.1.3: {} mustache@4.2.0: {} @@ -5687,6 +5993,8 @@ snapshots: negotiator@0.6.4: {} + negotiator@1.0.0: {} + node-fetch@2.6.13: dependencies: whatwg-url: 5.0.0 @@ -5736,6 +6044,10 @@ snapshots: on-headers@1.0.2: {} + once@1.4.0: + dependencies: + wrappy: 1.0.2 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -5794,6 +6106,8 @@ snapshots: path-to-regexp@6.3.0: {} + path-to-regexp@8.2.0: {} + pathe@1.1.2: {} pbkdf2@3.1.2: @@ -5979,6 +6293,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 @@ -6123,6 +6444,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.27.4 fsevents: 2.3.3 + router@2.0.0: + dependencies: + array-flatten: 3.0.0 + is-promise: 4.0.0 + methods: 1.1.2 + parseurl: 1.3.3 + path-to-regexp: 8.2.0 + setprototypeof: 1.2.0 + utils-merge: 1.0.1 + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -6187,6 +6518,23 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.1.0: + dependencies: + debug: 4.3.7 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime-types: 2.1.35 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + serve-static@1.16.2: dependencies: encodeurl: 2.0.0 @@ -6196,6 +6544,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.1.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.1.0 + transitivePeerDependencies: + - supports-color + set-cookie-parser@2.7.1: {} set-function-length@1.2.2: @@ -6439,6 +6796,12 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + type-is@2.0.0: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.0 + typed-array-buffer@1.0.2: dependencies: call-bind: 1.0.7 @@ -6708,6 +7071,8 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.0 + wrappy@1.0.2: {} + ws@8.18.0: {} xhr2@0.2.1: {} diff --git a/frontend/src/lib/components/StructureViewer/MolstarWrapper.ts b/frontend/src/lib/components/StructureViewer/MolstarWrapper.ts index 4265c81..5acedd7 100644 --- a/frontend/src/lib/components/StructureViewer/MolstarWrapper.ts +++ b/frontend/src/lib/components/StructureViewer/MolstarWrapper.ts @@ -8,6 +8,19 @@ import type { import type { RGB } from "$lib/utils"; +import { isEmptyLoci, Loci } from "molstar/lib/mol-model/loci"; +import { + QueryContext, + StructureElement, + StructureSelection, +} from "molstar/lib/mol-model/structure"; +import { Structure } from "molstar/lib/mol-model/structure/structure"; +import { StateTransforms } from "molstar/lib/mol-plugin-state/transforms"; +import { MolScriptBuilder as MS } from "molstar/lib/mol-script/language/builder"; +import { compile } from "molstar/lib/mol-script/runtime/query/compiler"; +import { StateSelection } from "molstar/lib/mol-state"; +import { Transparency } from "molstar/lib/mol-theme/transparency"; + export class MolstarWrapper { private viewer: any; private ready: boolean = false; @@ -181,6 +194,7 @@ export class MolstarWrapper { keepColors, keepRepresentations, ); + await this.applyConfidenceVisualization(); } async updateHighlightState(state: HighlightState | null) { @@ -196,6 +210,170 @@ export class MolstarWrapper { async applyConfidenceVisualization() { await this.whenReady(); - //await this.applyTransparency(70, 0.3); + await this.setTransparency(70); + } + + private getLociByPLDDT( + score: number, + contextData: Structure, + ): StructureElement.Loci { + const queryExp = MS.struct.modifier.union([ + MS.struct.modifier.wholeResidues([ + MS.struct.modifier.union([ + MS.struct.generator.atomGroups({ + "chain-test": MS.core.rel.eq([ + MS.ammp("objectPrimitive"), + "atomistic", + ]), + "residue-test": MS.core.rel.lte([ + MS.struct.atomProperty.macromolecular.B_iso_or_equiv(), + score, + ]), + }), + ]), + ]), + ]); + + const query = compile(queryExp); + const sel = query(new QueryContext(contextData)); + return StructureSelection.toLociWithSourceUnits(sel); + } + + private getFilteredBundle( + layers: Transparency.BundleLayer[], + structure: Structure, + ) { + const transparency = Transparency.ofBundle(layers, structure.root); + const merged = Transparency.merge(transparency); + return Transparency.filter( + merged, + structure, + ) as Transparency; + } + + async setTransparency(score: number, transparencyValue: number = 0.5) { + await this.whenReady(); + if (!this.plugin) return; + + // Wait for structure to be fully loaded and processed + await new Promise((resolve) => setTimeout(resolve, 100)); // Add small delay + + const hierarchy = this.plugin.managers.structure.hierarchy; + const current = hierarchy.current; + + if (!current?.structures?.[0]?.cell?.obj?.data) { + console.warn("No structure data available for transparency"); + return; + } + + const structure = current.structures[0]; + const loci = this.getLociByPLDDT(score, structure.cell.obj.data); + if (isEmptyLoci(loci)) { + console.warn("No matching loci found for transparency"); + return; + } + + await this.plugin.dataTransaction( + async () => { + await this.setStructureTransparency( + structure.components, + transparencyValue, + loci, + ); + }, + { canUndo: "Apply Transparency" }, + ); + } + + private async setStructureTransparency( + components: any[], + value: number, + loci: StructureElement.Loci, + ) { + if (!components?.length) { + console.warn("No components found for transparency"); + return; + } + + const state = this.plugin.state.data; + const update = state.build(); + + for (const c of components) { + if (!c?.representations?.length) continue; + + for (const r of c.representations) { + if (!r?.cell?.obj?.data?.sourceData) continue; + + const structure = r.cell.obj.data.sourceData; + if (Loci.isEmpty(loci) || isEmptyLoci(loci)) continue; + + const reprRef = r.cell.transform.ref; + const transparency = state.select( + StateSelection.Generators.ofTransformer( + StateTransforms.Representation + .TransparencyStructureRepresentation3DFromBundle, + reprRef, + ).withTag("transparency-controls"), + )[0]; + + const layer = { + bundle: StructureElement.Bundle.fromLoci(loci), + value, + }; + + try { + if (transparency) { + const bundleLayers = [...transparency.params!.values.layers, layer]; + const filtered = this.getFilteredBundle(bundleLayers, structure); + update.to(transparency).update(Transparency.toBundle(filtered)); + } else { + const filtered = this.getFilteredBundle([layer], structure); + update + .to(reprRef) + .apply( + StateTransforms.Representation + .TransparencyStructureRepresentation3DFromBundle, + Transparency.toBundle(filtered), + { tags: ["transparency-controls"] }, + ); + } + } catch (error) { + console.error("Error applying transparency:", error); + } + } + } + + return update.commit({ doNotUpdateCurrent: true }); + } + + async clearTransparency() { + await this.whenReady(); + if (!this.plugin) return; + + const structure = + this.viewer.viewerInstance.plugin.managers.structure.hierarchy.current + ?.structures[0]; + if (!structure) return; + + const state = this.plugin.state.data; + const update = state.build(); + + for (const c of structure.components) { + for (const r of c.representations) { + const transparency = state.select( + StateSelection.Generators.ofTransformer( + StateTransforms.Representation + .TransparencyStructureRepresentation3DFromBundle, + r.cell.transform.ref, + ).withTag("transparency-controls"), + )[0]; + + if (transparency) { + update.delete(transparency.transform.ref); + } + } + } + + await update.commit({ doNotUpdateCurrent: true }); } } diff --git a/frontend/src/lib/components/StructureViewer/StructureViewer.svelte b/frontend/src/lib/components/StructureViewer/StructureViewer.svelte index fe70bf7..b51be57 100644 --- a/frontend/src/lib/components/StructureViewer/StructureViewer.svelte +++ b/frontend/src/lib/components/StructureViewer/StructureViewer.svelte @@ -40,7 +40,6 @@ molstarWrapper.whenReady().then(() => { dispatch("viewerReady"); molstarWrapper.updateBackground(); - molstarWrapper.applyConfidenceVisualization(); }); }); From e6ec2bcbd2e92581d106453bd1b1f5196a659bb0 Mon Sep 17 00:00:00 2001 From: Tobias O Date: Thu, 23 Jan 2025 17:25:39 +0100 Subject: [PATCH 4/5] refactor: copy molstar implementation --- .../StructureViewer/MolstarWrapper.ts | 268 +++++++++--------- .../StructureViewer/StructureViewer.svelte | 1 + 2 files changed, 132 insertions(+), 137 deletions(-) diff --git a/frontend/src/lib/components/StructureViewer/MolstarWrapper.ts b/frontend/src/lib/components/StructureViewer/MolstarWrapper.ts index 5acedd7..9730d8b 100644 --- a/frontend/src/lib/components/StructureViewer/MolstarWrapper.ts +++ b/frontend/src/lib/components/StructureViewer/MolstarWrapper.ts @@ -9,18 +9,37 @@ import type { import type { RGB } from "$lib/utils"; import { isEmptyLoci, Loci } from "molstar/lib/mol-model/loci"; -import { - QueryContext, - StructureElement, - StructureSelection, -} from "molstar/lib/mol-model/structure"; +import { StructureElement } from "molstar/lib/mol-model/structure"; import { Structure } from "molstar/lib/mol-model/structure/structure"; +import type { StructureComponentRef } from "molstar/lib/mol-plugin-state/manager/structure/hierarchy-state"; +import type { PluginStateObject } from "molstar/lib/mol-plugin-state/objects"; import { StateTransforms } from "molstar/lib/mol-plugin-state/transforms"; -import { MolScriptBuilder as MS } from "molstar/lib/mol-script/language/builder"; -import { compile } from "molstar/lib/mol-script/runtime/query/compiler"; -import { StateSelection } from "molstar/lib/mol-state"; +import { PluginContext } from "molstar/lib/mol-plugin/context"; +import { + StateBuilder, + StateObjectCell, + StateSelection, + StateTransform, +} from "molstar/lib/mol-state"; import { Transparency } from "molstar/lib/mol-theme/transparency"; +type TransparencyEachReprCallback = ( + update: StateBuilder.Root, + repr: StateObjectCell< + PluginStateObject.Molecule.Structure.Representation3D, + StateTransform< + typeof StateTransforms.Representation.StructureRepresentation3D + > + >, + transparency?: StateObjectCell< + any, + StateTransform< + typeof StateTransforms.Representation.TransparencyStructureRepresentation3DFromBundle + > + >, +) => Promise; +const TransparencyManagerTag = "transparency-controls"; + export class MolstarWrapper { private viewer: any; private ready: boolean = false; @@ -194,7 +213,6 @@ export class MolstarWrapper { keepColors, keepRepresentations, ); - await this.applyConfidenceVisualization(); } async updateHighlightState(state: HighlightState | null) { @@ -210,74 +228,33 @@ export class MolstarWrapper { async applyConfidenceVisualization() { await this.whenReady(); - await this.setTransparency(70); - } - - private getLociByPLDDT( - score: number, - contextData: Structure, - ): StructureElement.Loci { - const queryExp = MS.struct.modifier.union([ - MS.struct.modifier.wholeResidues([ - MS.struct.modifier.union([ - MS.struct.generator.atomGroups({ - "chain-test": MS.core.rel.eq([ - MS.ammp("objectPrimitive"), - "atomistic", - ]), - "residue-test": MS.core.rel.lte([ - MS.struct.atomProperty.macromolecular.B_iso_or_equiv(), - score, - ]), - }), - ]), - ]), - ]); - - const query = compile(queryExp); - const sel = query(new QueryContext(contextData)); - return StructureSelection.toLociWithSourceUnits(sel); - } - - private getFilteredBundle( - layers: Transparency.BundleLayer[], - structure: Structure, - ) { - const transparency = Transparency.ofBundle(layers, structure.root); - const merged = Transparency.merge(transparency); - return Transparency.filter( - merged, - structure, - ) as Transparency; - } - - async setTransparency(score: number, transparencyValue: number = 0.5) { - await this.whenReady(); - if (!this.plugin) return; - - // Wait for structure to be fully loaded and processed - await new Promise((resolve) => setTimeout(resolve, 100)); // Add small delay - - const hierarchy = this.plugin.managers.structure.hierarchy; - const current = hierarchy.current; - if (!current?.structures?.[0]?.cell?.obj?.data) { - console.warn("No structure data available for transparency"); + const plugin = this.plugin; + if (!plugin) { + console.warn("No transparency plugin found"); return; } - const structure = current.structures[0]; - const loci = this.getLociByPLDDT(score, structure.cell.obj.data); - if (isEmptyLoci(loci)) { - console.warn("No matching loci found for transparency"); + const pLDDT = 70; + const transparency = 1; + + const assemblyRef = + this.plugin.managers.structure.hierarchy.current.structures[0].cell + .transform.ref; + const structure = + plugin.managers.structure.hierarchy.current?.refs.get(assemblyRef); + if (!structure) { + console.warn("No structure found"); return; } - await this.plugin.dataTransaction( - async () => { + return plugin.dataTransaction( + async (ctx: any) => { + const loci = this.viewer.viewerInstance.getLociByPLDDT(pLDDT); await this.setStructureTransparency( + plugin, structure.components, - transparencyValue, + transparency, loci, ); }, @@ -285,95 +262,112 @@ export class MolstarWrapper { ); } - private async setStructureTransparency( - components: any[], - value: number, - loci: StructureElement.Loci, + private getFilteredBundle( + layers: Transparency.BundleLayer[], + structureRef: Structure, ) { - if (!components?.length) { - console.warn("No components found for transparency"); - return; - } + const transparency = Transparency.ofBundle(layers, structureRef.root); + const merged = Transparency.merge(transparency); + return Transparency.filter( + merged, + structureRef, + ) as Transparency; + } - const state = this.plugin.state.data; + private async updateRepresentations( + plugin: PluginContext, + components: StructureComponentRef[], + callback: TransparencyEachReprCallback, + ) { + const state = plugin.state.data; const update = state.build(); for (const c of components) { - if (!c?.representations?.length) continue; - for (const r of c.representations) { - if (!r?.cell?.obj?.data?.sourceData) continue; - - const structure = r.cell.obj.data.sourceData; - if (Loci.isEmpty(loci) || isEmptyLoci(loci)) continue; - - const reprRef = r.cell.transform.ref; const transparency = state.select( StateSelection.Generators.ofTransformer( StateTransforms.Representation .TransparencyStructureRepresentation3DFromBundle, - reprRef, - ).withTag("transparency-controls"), - )[0]; - - const layer = { - bundle: StructureElement.Bundle.fromLoci(loci), - value, - }; + r.cell.transform.ref, + ).withTag(TransparencyManagerTag), + ); - try { - if (transparency) { - const bundleLayers = [...transparency.params!.values.layers, layer]; - const filtered = this.getFilteredBundle(bundleLayers, structure); - update.to(transparency).update(Transparency.toBundle(filtered)); - } else { - const filtered = this.getFilteredBundle([layer], structure); - update - .to(reprRef) - .apply( - StateTransforms.Representation - .TransparencyStructureRepresentation3DFromBundle, - Transparency.toBundle(filtered), - { tags: ["transparency-controls"] }, - ); - } - } catch (error) { - console.error("Error applying transparency:", error); - } + await callback(update, r.cell, transparency[0]); } } - return update.commit({ doNotUpdateCurrent: true }); + update.commit(); } - async clearTransparency() { - await this.whenReady(); - if (!this.plugin) return; - - const structure = - this.viewer.viewerInstance.plugin.managers.structure.hierarchy.current - ?.structures[0]; - if (!structure) return; + private async setStructureTransparency( + plugin: PluginContext, + components: StructureComponentRef[], + value: number, + loci: StructureElement.Loci, + ) { + await this.updateRepresentations( + plugin, + components, + async (update, repr, transparencyCell) => { + const structure = repr.obj!.data.sourceData; + if (Loci.isEmpty(loci) || isEmptyLoci(loci)) { + return; + } - const state = this.plugin.state.data; - const update = state.build(); + const layer = { + bundle: StructureElement.Bundle.fromLoci(loci), + value, + }; - for (const c of structure.components) { - for (const r of c.representations) { - const transparency = state.select( - StateSelection.Generators.ofTransformer( - StateTransforms.Representation - .TransparencyStructureRepresentation3DFromBundle, - r.cell.transform.ref, - ).withTag("transparency-controls"), - )[0]; + if (transparencyCell) { + const bundleLayers = [ + ...transparencyCell.params!.values.layers, + layer, + ]; + const filtered = this.getFilteredBundle(bundleLayers, structure); + update.to(transparencyCell).update(Transparency.toBundle(filtered)); + } else { + const parentRef = repr.transform.ref; + const parentNode = plugin.state.data.tree.transforms.get(parentRef); + if (!parentNode) { + throw new Error(`Parent node ${parentRef} not found in state tree`); + } - if (transparency) { - update.delete(transparency.transform.ref); + // Create the transparency transform params + const filtered = this.getFilteredBundle([layer], structure); + const transparencyRef = `transparency-${parentRef}`; + const params = { + ...Transparency.toBundle(filtered), + parent: parentRef, + }; + update + .to(parentRef) + .apply( + StateTransforms.Representation + .TransparencyStructureRepresentation3DFromBundle, + params, + { + tags: TransparencyManagerTag, + ref: transparencyRef, + }, + ); } - } - } + }, + ); + } - await update.commit({ doNotUpdateCurrent: true }); + private async clearTransparency( + plugin: PluginContext, + components: StructureComponentRef[], + ) { + await this.updateRepresentations( + plugin, + components, + async (update, repr, transparencyCell) => { + if (transparencyCell) { + update.delete(transparencyCell.transform.ref); + } + }, + ); } } diff --git a/frontend/src/lib/components/StructureViewer/StructureViewer.svelte b/frontend/src/lib/components/StructureViewer/StructureViewer.svelte index b51be57..1b6d4e1 100644 --- a/frontend/src/lib/components/StructureViewer/StructureViewer.svelte +++ b/frontend/src/lib/components/StructureViewer/StructureViewer.svelte @@ -35,6 +35,7 @@ selectionUnsubscribe = state.selectionStore.subscribe(async (state) => { await molstarWrapper.updateSelectionState(state); + await molstarWrapper.applyConfidenceVisualization(); }); molstarWrapper.whenReady().then(() => { From 5529ee4066f929cd7bd9e95eb9a7311b90ea6c2a Mon Sep 17 00:00:00 2001 From: Tobias O Date: Thu, 6 Feb 2025 18:13:20 +0100 Subject: [PATCH 5/5] chore: change molstar to same version --- frontend/package.json | 2 +- frontend/pnpm-lock.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index c7b57a5..360b75d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -58,7 +58,7 @@ "d3": "^7.9.0", "d3-cloud": "^1.2.7", "iconify-icon": "^2.3.0", - "molstar": "^4.11.0", + "molstar": "=4.8.0", "pdbe-molstar": "^3.3.2" } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 2f7eb2d..e1188e1 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -63,7 +63,7 @@ importers: specifier: ^2.3.0 version: 2.3.0 molstar: - specifier: ^4.11.0 + specifier: ^4.8.0 version: 4.11.0(@types/react@18.3.9)(fp-ts@2.16.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) pdbe-molstar: specifier: ^3.3.2 @@ -7107,7 +7107,7 @@ snapshots: send@1.1.0: dependencies: - debug: 4.3.6 + debug: 4.4.0 destroy: 1.2.0 encodeurl: 2.0.0 escape-html: 1.0.3