mirror of
https://github.com/molstar/molstar.git
synced 2026-06-07 07:04:22 +08:00
Compare commits
243 Commits
app-load-u
...
kinemages
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3b54ff88c | ||
|
|
3e7614d75c | ||
|
|
a01e8f26bd | ||
|
|
351faf3c45 | ||
|
|
f500372c16 | ||
|
|
2714d32e15 | ||
|
|
2d400d9166 | ||
|
|
ebceecb3e6 | ||
|
|
a87f92bf7d | ||
|
|
4033bc93c2 | ||
|
|
6c4ba7af61 | ||
|
|
bc9584e49b | ||
|
|
550d898c4f | ||
|
|
5e16c340dc | ||
|
|
bcb18a8faf | ||
|
|
cd7d8f704e | ||
|
|
0d197b2dc5 | ||
|
|
b3ce268f0e | ||
|
|
2da02daadc | ||
|
|
999e5a47af | ||
|
|
ff3fad0789 | ||
|
|
5af6265c07 | ||
|
|
4ad7a96191 | ||
|
|
71215d183d | ||
|
|
e864f13a66 | ||
|
|
59298be573 | ||
|
|
2a57867167 | ||
|
|
a817a70b46 | ||
|
|
5afb7981ae | ||
|
|
2308dfd22e | ||
|
|
f0de2cea2c | ||
|
|
8f9c687935 | ||
|
|
1f56405d79 | ||
|
|
37af8c66a1 | ||
|
|
a1fefa2efa | ||
|
|
958f3011e4 | ||
|
|
8d48fa67ae | ||
|
|
87158db7c0 | ||
|
|
bac65cc71e | ||
|
|
3ece0c74c6 | ||
|
|
89fbb690fe | ||
|
|
918d02fec4 | ||
|
|
8af3a240b5 | ||
|
|
a80284ac02 | ||
|
|
1b48f4c32a | ||
|
|
1055eab4c5 | ||
|
|
d4445cef5c | ||
|
|
063d327a5f | ||
|
|
34f409e683 | ||
|
|
8415ed1b92 | ||
|
|
c92147289e | ||
|
|
5b16213cd2 | ||
|
|
bbd36e1838 | ||
|
|
1a0b30d6eb | ||
|
|
e309e8917a | ||
|
|
14135b8386 | ||
|
|
3230a6a7dc | ||
|
|
34ebc5ab7a | ||
|
|
d8b62c5cbb | ||
|
|
358ef44780 | ||
|
|
fb2f79a395 | ||
|
|
e3a95e0a08 | ||
|
|
a1b09ccc1c | ||
|
|
26b31b3fcc | ||
|
|
3027418d31 | ||
|
|
5a69fb691d | ||
|
|
cced98c93f | ||
|
|
6c299161fe | ||
|
|
10575ac361 | ||
|
|
d715330d8e | ||
|
|
fdba049982 | ||
|
|
270d7386b2 | ||
|
|
a28c2f0995 | ||
|
|
196e17ff0d | ||
|
|
c7efac0a78 | ||
|
|
75eb04070c | ||
|
|
d6c9ae1fbe | ||
|
|
24a6403025 | ||
|
|
842e5d890e | ||
|
|
b34d1cca00 | ||
|
|
af4dc090c4 | ||
|
|
1fa090d162 | ||
|
|
4d5b749e3e | ||
|
|
6a736eb89f | ||
|
|
cfead0481f | ||
|
|
fbbd7e623e | ||
|
|
f16707b849 | ||
|
|
76b0b23c07 | ||
|
|
d4a2bd7cba | ||
|
|
c572feb1d2 | ||
|
|
121f8eab3e | ||
|
|
8c49b82c3d | ||
|
|
ac7faf8524 | ||
|
|
5d6d91a331 | ||
|
|
d476db556d | ||
|
|
354d092834 | ||
|
|
5cde26a8e2 | ||
|
|
b905178395 | ||
|
|
543d014d0d | ||
|
|
a17afa59b3 | ||
|
|
bd6be354d5 | ||
|
|
2058d605c7 | ||
|
|
ba60188758 | ||
|
|
52f2ddf715 | ||
|
|
aa20fffbfb | ||
|
|
b59d11c91a | ||
|
|
8fdc29d048 | ||
|
|
d52ea41051 | ||
|
|
852be261dd | ||
|
|
95e9a3012d | ||
|
|
dd0a45c154 | ||
|
|
4692d63a2b | ||
|
|
0689ecabb6 | ||
|
|
424f576e99 | ||
|
|
4407994195 | ||
|
|
4c6331e72d | ||
|
|
825514dd10 | ||
|
|
8e2967b993 | ||
|
|
a46ba63e31 | ||
|
|
cf9fe99a81 | ||
|
|
2909a209c3 | ||
|
|
1322882444 | ||
|
|
119c548fa7 | ||
|
|
539442f710 | ||
|
|
8841f04af6 | ||
|
|
8c2d3a577a | ||
|
|
bed4b728d3 | ||
|
|
aa87acc0a7 | ||
|
|
7d1e2b44db | ||
|
|
c1c1badf62 | ||
|
|
e270a83909 | ||
|
|
a40b737c6f | ||
|
|
942533ed2b | ||
|
|
546f3cd3c5 | ||
|
|
21597b1fdd | ||
|
|
31d8568c1a | ||
|
|
3630cd14e8 | ||
|
|
4f083f10e6 | ||
|
|
371ef984c0 | ||
|
|
e2db1257cd | ||
|
|
c812e72a1a | ||
|
|
ef9b89820d | ||
|
|
5fd453c77a | ||
|
|
7f2b10674e | ||
|
|
238e5e0b88 | ||
|
|
1f26b5c339 | ||
|
|
eac478e7cb | ||
|
|
d0c59fdc92 | ||
|
|
7e61bcad32 | ||
|
|
7e98870dce | ||
|
|
405dc0d90a | ||
|
|
7a362c816e | ||
|
|
7e1396b74c | ||
|
|
68ad1ec065 | ||
|
|
430f8da44e | ||
|
|
68866cd2de | ||
|
|
05888bec50 | ||
|
|
65e1cb4a5d | ||
|
|
50f571b0d3 | ||
|
|
d86b31edf8 | ||
|
|
ec107352b4 | ||
|
|
1d42d5a2d6 | ||
|
|
02d1dcb9d9 | ||
|
|
d86c3621b7 | ||
|
|
f2724491c2 | ||
|
|
f4c84a6930 | ||
|
|
0eb9b286b4 | ||
|
|
da006391da | ||
|
|
130e33f8c3 | ||
|
|
109e528d1c | ||
|
|
5df69cd84a | ||
|
|
973afa2237 | ||
|
|
0088d3e1bf | ||
|
|
26e6a11fa8 | ||
|
|
056e2c5182 | ||
|
|
0e7cde24bc | ||
|
|
36ce262970 | ||
|
|
289c8181c8 | ||
|
|
eb0fd490d4 | ||
|
|
70c073c43c | ||
|
|
0565df4df9 | ||
|
|
6dd425cb55 | ||
|
|
c0f994a506 | ||
|
|
63705ed158 | ||
|
|
fda9069f17 | ||
|
|
66bffd8403 | ||
|
|
4a7d83c85b | ||
|
|
fef649ce09 | ||
|
|
794f81bb8e | ||
|
|
bc5648620d | ||
|
|
07897f57f3 | ||
|
|
77f756dfe0 | ||
|
|
15eef7b688 | ||
|
|
5d0ba7504b | ||
|
|
8d59b5b814 | ||
|
|
86871124d5 | ||
|
|
1a328d98b6 | ||
|
|
fcbc3ab3d0 | ||
|
|
f36093dad9 | ||
|
|
d4c2bb85cb | ||
|
|
d53c1e8e65 | ||
|
|
f189d0bdab | ||
|
|
395eddd927 | ||
|
|
eb1d48a73c | ||
|
|
38b0bb8d7d | ||
|
|
0daffa6b57 | ||
|
|
e1d5d369f1 | ||
|
|
6b88acd2bc | ||
|
|
6384eac7e7 | ||
|
|
fd409ce27f | ||
|
|
c21c9f5160 | ||
|
|
71d00a22dd | ||
|
|
5554697028 | ||
|
|
709ac8430a | ||
|
|
2ee08f6161 | ||
|
|
3608578528 | ||
|
|
5114a211fd | ||
|
|
8cc947c998 | ||
|
|
9105737834 | ||
|
|
84bcbd1ca6 | ||
|
|
144967dbd3 | ||
|
|
bdb33b9398 | ||
|
|
ade6ef5631 | ||
|
|
3bc60d1d59 | ||
|
|
4068c45eb4 | ||
|
|
52d0ff4a67 | ||
|
|
f346d15bef | ||
|
|
b2ce1fc6fa | ||
|
|
4e331001ef | ||
|
|
7d67304e4c | ||
|
|
babda601cb | ||
|
|
8bdfff5e94 | ||
|
|
be4b408ddc | ||
|
|
230697fbb4 | ||
|
|
78ab6b0c95 | ||
|
|
1f0c24b58e | ||
|
|
f5c587bfe5 | ||
|
|
52b1c7e4d9 | ||
|
|
e76d02bc8c | ||
|
|
481b763049 | ||
|
|
7bfef2ae40 | ||
|
|
01fe10ebdc | ||
|
|
4e4b80a7b2 |
11
CHANGELOG.md
11
CHANGELOG.md
@@ -4,10 +4,7 @@ All notable changes to this project will be documented in this file, following t
|
||||
Note that since we don't clearly distinguish between a public and private interfaces there will be changes in non-major versions that are potentially breaking. If we make breaking changes to less used interfaces we will highlight it in here.
|
||||
|
||||
## [Unreleased]
|
||||
- Fix exported image artifacts on transparent background with emissive, bloom, or antialiasing
|
||||
- Fix cel-shaded ambient color being stripped to luminance (now uses full RGB, matching the classic lighting path)
|
||||
- Fix empty transforms default in `ShapeFromPly`
|
||||
- Use morton order for spheres in dot visual with lod-levels
|
||||
- Add `Camera.changed` event and rotation/translation setter/getter
|
||||
- Add `instanceGranularity: 'auto'` as a memory guard
|
||||
- Honor `instanceGranularity` in `Visual.getLoci`
|
||||
@@ -15,14 +12,6 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- Add presets option to `ObjectList` param definition
|
||||
- Fix memory leak in `State.dispose()` not invoking transformer `dispose` callbacks for live cells
|
||||
- Adds File/Open and drag-and-drop support for Kinemage files in the viewer app
|
||||
- Fix bugs in ModelServer surroundingLigands endpoint, resulting in omitWater not honored
|
||||
- Fix `Volume` and `Isosurface` getBoundingSphere ignoring instances
|
||||
- Fix aromatic ring detection not accounting for hybridization
|
||||
- Add axis param to camera spin/rock animation
|
||||
- Fix SSAO half/quarter resolution textures for multi-scale
|
||||
- Non-covalent interactions: water bridge support
|
||||
- Download Structure From AlphaFoldDB allows IDs with version suffix (version is ignored)
|
||||
- Add `loadUrl` method and GET params to Viewer app
|
||||
|
||||
## [v5.9.0] - 2026-05-03
|
||||
- Fix edge case when `PluginSpec.animations` is empty
|
||||
|
||||
@@ -14,7 +14,7 @@ import { MVSData } from '../../extensions/mvs/mvs-data';
|
||||
import { StringLike } from '../../mol-io/common/string-like';
|
||||
import { Structure, StructureElement } from '../../mol-model/structure';
|
||||
import { Volume } from '../../mol-model/volume';
|
||||
import { DownloadFile, OpenFiles } from '../../mol-plugin-state/actions/file';
|
||||
import { OpenFiles } from '../../mol-plugin-state/actions/file';
|
||||
import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure';
|
||||
import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
|
||||
import { PresetTrajectoryHierarchy } from '../../mol-plugin-state/builder/structure/hierarchy-preset';
|
||||
@@ -523,17 +523,6 @@ export class Viewer {
|
||||
}
|
||||
}
|
||||
|
||||
loadUrl(url: string, format: string, isBinary = false) {
|
||||
return this.plugin.runTask(Task.create('Load URL', async taskCtx => {
|
||||
await this.plugin.state.data.applyAction(DownloadFile, {
|
||||
url: Asset.Url(url),
|
||||
format,
|
||||
isBinary,
|
||||
visuals: true
|
||||
}).runInContext(taskCtx);
|
||||
}));
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
this.plugin.layout.events.updated.next(void 0);
|
||||
}
|
||||
|
||||
@@ -129,11 +129,6 @@
|
||||
var modelArchive = getParam('model-archive', '[^&]+').trim();
|
||||
if (modelArchive) viewer.loadModelArchive(modelArchive);
|
||||
|
||||
var url = getParam('url', '[^&]+').trim();
|
||||
var urlFormat = getParam('url-format', '[^&]+').trim() || undefined;
|
||||
var urlIsBinary = getParam('url-is-binary', '[^&]+').trim() === '1';
|
||||
if (url && urlFormat) viewer.loadUrl(url, urlFormat, urlIsBinary);
|
||||
|
||||
window.addEventListener('unload', () => {
|
||||
// to aid GC
|
||||
viewer.dispose();
|
||||
|
||||
@@ -25,7 +25,6 @@ export type InteractionElementSchema =
|
||||
| { kind: 'weak-hydrogen-bond' } & InteractionElementSchemaBase
|
||||
| { kind: 'hydrophobic' } & InteractionElementSchemaBase
|
||||
| { kind: 'metal-coordination' } & InteractionElementSchemaBase
|
||||
| { kind: 'water-bridge' } & InteractionElementSchemaBase
|
||||
| { kind: 'covalent', degree?: 'aromatic' | 1 | 2 | 3 | 4 } & InteractionElementSchemaBase
|
||||
|
||||
export type InteractionKind = InteractionElementSchema['kind']
|
||||
@@ -40,7 +39,6 @@ export const InteractionKinds: InteractionKind[] = [
|
||||
'weak-hydrogen-bond',
|
||||
'hydrophobic',
|
||||
'metal-coordination',
|
||||
'water-bridge',
|
||||
'covalent',
|
||||
];
|
||||
|
||||
@@ -54,7 +52,6 @@ export type InteractionInfo =
|
||||
| { kind: 'weak-hydrogen-bond', hydrogenStructureRef?: string, hydrogen?: StructureElement.Loci }
|
||||
| { kind: 'hydrophobic' }
|
||||
| { kind: 'metal-coordination' }
|
||||
| { kind: 'water-bridge' }
|
||||
| { kind: 'covalent', degree?: 'aromatic' | 1 | 2 | 3 | 4 }
|
||||
|
||||
export interface StructureInteractionElement {
|
||||
@@ -83,5 +80,4 @@ export const InteractionTypeToKind = {
|
||||
[InteractionType.Hydrophobic]: 'hydrophobic' as InteractionKind,
|
||||
[InteractionType.MetalCoordination]: 'metal-coordination' as InteractionKind,
|
||||
[InteractionType.WeakHydrogenBond]: 'weak-hydrogen-bond' as InteractionKind,
|
||||
[InteractionType.WaterBridge]: 'water-bridge' as InteractionKind,
|
||||
};
|
||||
@@ -47,7 +47,6 @@ export const InteractionVisualParams = {
|
||||
'weak-hydrogen-bond': hydrogenVisualParams({ color: Color(0x0) }),
|
||||
'hydrophobic': visualParams({ color: Color(0x555555) }),
|
||||
'metal-coordination': visualParams({ color: Color(0x952e8f) }),
|
||||
'water-bridge': visualParams({ color: Color(0x00CCEE), style: 'dashed' }),
|
||||
'covalent': PD.Group({
|
||||
color: PD.Color(Color(0x999999)),
|
||||
radius: PD.Numeric(0.1, { min: 0.01, max: 1, step: 0.01 }),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author ReliaSolve <russ@reliasolve.com>
|
||||
*/
|
||||
@@ -23,76 +23,29 @@ const kinString = `@kinemage 1
|
||||
{"}hotpink P 'O' 32.729,45.605,11.052 {"}hotpink 'O' 32.572,45.765,11.173
|
||||
`;
|
||||
|
||||
// Complex kinemage with multiple features: animate groups, pointmasters, various list types
|
||||
// @todo Replace with more complex kinemage
|
||||
const kinComplexString = `@kinemage 1
|
||||
@caption Complex test kinemage with multiple features
|
||||
@text
|
||||
This is a comprehensive test kinemage file that includes:
|
||||
- Multiple groups with animate and 2animate
|
||||
- Pointmasters with tags
|
||||
- All list types: dots, vectors, balls, spheres, ribbons, triangles
|
||||
@master {main} on
|
||||
@master {secondary} off
|
||||
@master {alternate}
|
||||
@pointmaster 'ABC' {Primary atoms} on
|
||||
@pointmaster 'XY' {Secondary atoms} off
|
||||
@group {Structure} animate dominant
|
||||
@subgroup {Backbone}
|
||||
@vectorlist {CA trace} color=blue master={main}
|
||||
{CA ALA 1}blue P 'A' 10.0,20.0,30.0 {CA ALA 2}blue 'A' 11.0,21.0,31.0
|
||||
{"}blue 'A' 12.0,22.0,32.0 {CA ALA 3}blue 'A' 13.0,23.0,33.0
|
||||
@dotlist {H-bonds} color=yellow master={main}
|
||||
{HN ALA 2}yellow 'B' 10.5,20.5,30.5
|
||||
{HN ALA 3}yellow 'B' 11.5,21.5,31.5
|
||||
{HN ALA 4}yellow 'B' 12.5,22.5,32.5
|
||||
@subgroup {Sidechains}
|
||||
@balllist {CB atoms} color=green master={secondary} radius=0.5
|
||||
{CB ARG 1}green r=0.5 'C' 9.0,19.0,29.0
|
||||
{CB ARG 2}green r=0.6 'C' 10.0,20.0,30.0
|
||||
{CB ARG 3}green r=0.55 'C' 11.0,21.0,31.0
|
||||
@group {Alternate conformations} 2animate
|
||||
@subgroup {Alt A}
|
||||
@spherelist {Waters A} color=cyan master={alternate} radius=1.0
|
||||
{HOH 101}cyan r=1.0 'X' 15.0,25.0,35.0
|
||||
{HOH 102}cyan r=1.2 'X' 16.0,26.0,36.0
|
||||
@subgroup {Alt B}
|
||||
@spherelist {Waters B} color=magenta master={alternate} radius=1.0
|
||||
{HOH 101}magenta r=1.0 'Y' 15.2,25.2,35.2
|
||||
{HOH 102}magenta r=1.1 'Y' 16.1,26.1,36.1
|
||||
@group {Surface} off
|
||||
@subgroup {Ribbons}
|
||||
@ribbonlist {Alpha helix} color=red master={main}
|
||||
{ASP 5}red 14.0,24.0,34.0
|
||||
{GLU 6}red 15.0,25.0,35.0
|
||||
{LYS 7}red 16.0,26.0,36.0
|
||||
{ARG 8}red 17.0,27.0,37.0
|
||||
{THR 9}red P 18.0,28.0,38.0
|
||||
{VAL 10}red 19.0,29.0,39.0
|
||||
@subgroup {Triangles}
|
||||
@trianglelist {Surface patch} color=sky master={secondary}
|
||||
{Tri 1}sky 20.0,30.0,40.0
|
||||
{Tri 1}sky 21.0,30.0,40.0
|
||||
{Tri 1}sky 20.5,31.0,40.0
|
||||
{Tri 2}sky X 22.0,32.0,42.0
|
||||
{Tri 2}sky 23.0,32.0,42.0
|
||||
{Tri 2}sky 22.5,33.0,42.0
|
||||
@group {Contacts} animate
|
||||
@subgroup {Clashes}
|
||||
@vectorlist {Bad overlaps} color=hotpink master={main} width=4
|
||||
{O HOH 319 A}hotpink P 31.146,32.100,-1.425 {O HOH 320 A}hotpink 31.015,32.234,-1.324
|
||||
{"}hotpink P 31.607,32.750,-1.156 {"}hotpink 31.410,32.784,-1.097
|
||||
@caption probe.2.26.021123, run Tue Apr 23 14:49:17 2024
|
||||
command: C:\tmp\cctbx_phenix\build\probe\exe\probe.exe -kin -mc -het -once -wat2wat -onlybadout -stdbonds water all 1ssxFH.pdb
|
||||
@group dominant {dots}
|
||||
@subgroup dominant {once dots}
|
||||
@master {bad overlap}
|
||||
@pointmaster 'O' {Hets contacts}
|
||||
@vectorlist {x} color=red master={bad overlap}
|
||||
{ O HOH 319 A}hotpink P 'O' 31.146,32.100,-1.425 {"}hotpink 'O' 31.015,32.234,-1.324
|
||||
{"}hotpink P 'O' 31.607,32.750,-1.156 {"}hotpink 'O' 31.410,32.784,-1.097
|
||||
{"}hotpink P 'O' 31.263,32.074,-1.185 {"}hotpink 'O' 31.117,32.209,-1.122
|
||||
{ O BHOH 338 A}hotpink P 'O' 32.540,45.631,10.833 {"}hotpink 'O' 32.430,45.771,10.977
|
||||
{"}hotpink P 'O' 32.316,45.500,10.828 {"}hotpink 'O' 32.230,45.689,10.998
|
||||
{"}hotpink P 'O' 32.068,45.424,10.824 {"}hotpink 'O' 32.034,45.604,10.975
|
||||
{"}hotpink P 'O' 32.729,45.605,11.052 {"}hotpink 'O' 32.572,45.765,11.173
|
||||
`;
|
||||
|
||||
describe('kin reader', () => {
|
||||
it('basic', async () => {
|
||||
const parsed = await parseKin(kinString).run();
|
||||
if (parsed.isError) {
|
||||
console.error('Parse error:', parsed);
|
||||
fail('Parse should not error');
|
||||
}
|
||||
if (parsed.result.length !== 1) {
|
||||
fail(`Expected 1 kinemage, got ${parsed.result.length}`);
|
||||
}
|
||||
if (parsed.isError) return;
|
||||
if (parsed.result.length !== 1) return;
|
||||
const kinemage = parsed.result[0];
|
||||
|
||||
const vectors = kinemage.vectorLists;
|
||||
@@ -100,89 +53,18 @@ describe('kin reader', () => {
|
||||
|
||||
const element = vectors[0];
|
||||
expect(element.name).toEqual('x');
|
||||
expect(element.position1Array.length).toEqual(7*3);
|
||||
expect(element.position1Array.length).toEqual(7);
|
||||
|
||||
// Test that colors are parsed correctly
|
||||
expect(element.color1Array.length).toEqual(7);
|
||||
// TODO: Add more tests
|
||||
|
||||
// Test masters are set up
|
||||
expect(element.masterArray).toContain('bad overlap');
|
||||
|
||||
expect.assertions(5);
|
||||
expect.assertions(3);
|
||||
});
|
||||
|
||||
it('complex', async () => {
|
||||
const parsed = await parseKin(kinComplexString).run();
|
||||
if (parsed.isError) {
|
||||
fail('Parse should not error');
|
||||
}
|
||||
if (parsed.isError) return;
|
||||
|
||||
expect(parsed.result.length).toBeGreaterThan(0);
|
||||
const kinemage = parsed.result[0];
|
||||
// TODO: Add more complex tests
|
||||
|
||||
// Verify structure is valid
|
||||
expect(kinemage.vectorLists).toBeDefined();
|
||||
expect(kinemage.masterDict).toBeDefined();
|
||||
expect(kinemage.groupDict).toBeDefined();
|
||||
expect(kinemage.pointmasterDict).toBeDefined();
|
||||
|
||||
// Test animate groups
|
||||
expect(kinemage.groupsAnimate.length).toEqual(2);
|
||||
expect(kinemage.groupsAnimate).toContain('Structure');
|
||||
expect(kinemage.groupsAnimate).toContain('Contacts');
|
||||
expect(kinemage.activeAnimateGroup).toEqual(0);
|
||||
|
||||
// Test 2animate groups
|
||||
expect(kinemage.groupsAnimate2.length).toEqual(1);
|
||||
expect(kinemage.groupsAnimate2).toContain('Alternate conformations');
|
||||
expect(kinemage.activeAnimateGroup2).toEqual(0);
|
||||
|
||||
// Test pointmasters
|
||||
expect(Object.keys(kinemage.pointmasterDict).length).toBeGreaterThan(0);
|
||||
expect(kinemage.pointmasterDict['A']).toEqual('Primary atoms');
|
||||
expect(kinemage.pointmasterDict['B']).toEqual('Primary atoms');
|
||||
expect(kinemage.pointmasterDict['X']).toEqual('Secondary atoms');
|
||||
|
||||
// Test masters
|
||||
expect(kinemage.masterDict['main']).toBeDefined();
|
||||
expect(kinemage.masterDict['main'].visible).toEqual(true);
|
||||
expect(kinemage.masterDict['secondary']).toBeDefined();
|
||||
expect(kinemage.masterDict['secondary'].visible).toEqual(false);
|
||||
|
||||
// Test list types
|
||||
expect(kinemage.vectorLists.length).toEqual(2);
|
||||
expect(kinemage.dotLists.length).toEqual(1);
|
||||
expect(kinemage.ballLists.length).toEqual(3); // 1 balllist + 2 spherelists
|
||||
expect(kinemage.ribbonLists.length).toEqual(2); // 1 ribbonlist + 1 trianglelist
|
||||
|
||||
// Test specific list properties
|
||||
const caTrace = kinemage.vectorLists.find(v => v.name === 'CA trace');
|
||||
expect(caTrace).toBeDefined();
|
||||
expect(caTrace?.masterArray).toContain('main');
|
||||
|
||||
const hBonds = kinemage.dotLists[0];
|
||||
expect(hBonds.name).toEqual('H-bonds');
|
||||
expect(hBonds.positionArray.length).toEqual(9); // 3 dots * 3 coords
|
||||
|
||||
const cbAtoms = kinemage.ballLists.find(b => b.name === 'CB atoms');
|
||||
expect(cbAtoms).toBeDefined();
|
||||
expect(cbAtoms?.radiusArray.length).toEqual(3);
|
||||
|
||||
const helix = kinemage.ribbonLists.find(r => r.name === 'Alpha helix');
|
||||
expect(helix).toBeDefined();
|
||||
expect(helix?.pairTriangleNormals).toEqual(true); // ribbonlist
|
||||
|
||||
const surface = kinemage.ribbonLists.find(r => r.name === 'Surface patch');
|
||||
expect(surface).toBeDefined();
|
||||
expect(surface?.pairTriangleNormals).toEqual(false); // trianglelist
|
||||
|
||||
// Test groups
|
||||
expect(Object.keys(kinemage.groupDict).length).toEqual(4);
|
||||
expect(kinemage.groupDict['Structure'].animate).toEqual(true);
|
||||
expect(kinemage.groupDict['Alternate conformations']['2animate']).toEqual(true);
|
||||
expect(kinemage.groupDict['Surface'].off).toEqual(true);
|
||||
|
||||
expect.assertions(38);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -36,20 +36,6 @@ const Transform = StateTransformer.builderFactory('sb-kinemage');
|
||||
*/
|
||||
export class KinemageObject extends PluginStateObject.Create<KinemageData>({ name: 'Kinemage', typeClass: 'Object' }) { }
|
||||
|
||||
/**
|
||||
* Visibility state for kinemage elements - stores which items are VISIBLE (not hidden)
|
||||
*/
|
||||
export interface KinemageVisibilityState {
|
||||
/** Map of group name -> visibility (true = visible, false = hidden/off) */
|
||||
groupVisibility: Map<string, boolean>;
|
||||
/** Map of subgroup name -> visibility (true = visible, false = hidden/off) */
|
||||
subgroupVisibility: Map<string, boolean>;
|
||||
/** Map of master name -> visibility (true = visible, false = hidden) */
|
||||
masterVisibility: Map<string, boolean>;
|
||||
activeAnimateGroup: number;
|
||||
activeAnimateGroup2: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a saved snapshot object (from a view state node) to the plugin camera.
|
||||
* Use PluginCommands.Camera.SetSnapshot so transitions and canvas props are handled properly.
|
||||
@@ -162,7 +148,7 @@ export const ParseKinemage = Transform({
|
||||
}
|
||||
|
||||
const label = params.label || data.kinemages[0]?.caption || 'Kinemage';
|
||||
return new KinemageObject(data, { label, description: `Kinemage with ${data.kinemages.length} kinemage(s)` });
|
||||
return new KinemageObject(data, { label, description: `Kinemage with ${data.kinemages.length} view(s)` });
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -201,77 +187,6 @@ export const SelectKinemage = Transform({
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Visibility Controller Transform - centralizes visibility state for all shape types
|
||||
* Stores visibility as key-value pairs where key is the item name and value is boolean (true = visible)
|
||||
*/
|
||||
export const KinemageVisibilityController = Transform({
|
||||
name: 'sb-kinemage-visibility-controller',
|
||||
display: { name: 'Kinemage Visibility Controller' },
|
||||
from: PluginStateObject.Format.Json,
|
||||
to: PluginStateObject.Format.Json,
|
||||
params: (a) => {
|
||||
const kinData = (a?.data as any)?.kinData as Kinemage | undefined;
|
||||
if (!kinData) {
|
||||
return {
|
||||
groupVisibility: PD.Value<{ [key: string]: boolean }>({}),
|
||||
subgroupVisibility: PD.Value<{ [key: string]: boolean }>({}),
|
||||
masterVisibility: PD.Value<{ [key: string]: boolean }>({}),
|
||||
activeAnimateGroup: PD.Numeric(0, { min: 0, max: 0, step: 1 }, { description: 'Active animate group index' }),
|
||||
activeAnimateGroup2: PD.Numeric(0, { min: 0, max: 0, step: 1 }, { description: 'Active animate2 group index' })
|
||||
};
|
||||
}
|
||||
|
||||
// Build initial visibility from parsed data
|
||||
const groupVisibility: { [key: string]: boolean } = {};
|
||||
const subgroupVisibility: { [key: string]: boolean } = {};
|
||||
const masterVisibility: { [key: string]: boolean } = {};
|
||||
|
||||
for (const [groupKey, groupInfo] of Object.entries(kinData.groupDict)) {
|
||||
groupVisibility[groupKey] = !(groupInfo as any).off;
|
||||
}
|
||||
|
||||
for (const [subgroupKey, subgroupInfo] of Object.entries(kinData.subgroupDict)) {
|
||||
subgroupVisibility[subgroupKey] = !(subgroupInfo as any).off;
|
||||
}
|
||||
|
||||
for (const [masterKey, masterInfo] of Object.entries(kinData.masterDict)) {
|
||||
masterVisibility[masterKey] = !!(masterInfo as any).visible;
|
||||
}
|
||||
|
||||
return {
|
||||
groupVisibility: PD.Value(groupVisibility, { isHidden: true }),
|
||||
subgroupVisibility: PD.Value(subgroupVisibility, { isHidden: true }),
|
||||
masterVisibility: PD.Value(masterVisibility, { isHidden: true }),
|
||||
activeAnimateGroup: PD.Numeric(0, { min: 0, max: Math.max(0, kinData.groupsAnimate.length - 1), step: 1 }, { description: 'Active animate group index', isHidden: true }),
|
||||
activeAnimateGroup2: PD.Numeric(0, { min: 0, max: Math.max(0, kinData.groupsAnimate2.length - 1), step: 1 }, { description: 'Active animate2 group index', isHidden: true })
|
||||
};
|
||||
}
|
||||
})({
|
||||
apply({ a, params }) {
|
||||
return Task.create('Kinemage Visibility Controller', async ctx => {
|
||||
const kinData = (a.data as any).kinData as Kinemage;
|
||||
if (!kinData) {
|
||||
throw new Error('No kinData found in parent Format.Json node');
|
||||
}
|
||||
|
||||
// Store visibility state alongside kinData
|
||||
const visibilityState: KinemageVisibilityState = {
|
||||
groupVisibility: new Map(Object.entries(params.groupVisibility)),
|
||||
subgroupVisibility: new Map(Object.entries(params.subgroupVisibility)),
|
||||
masterVisibility: new Map(Object.entries(params.masterVisibility)),
|
||||
activeAnimateGroup: params.activeAnimateGroup,
|
||||
activeAnimateGroup2: params.activeAnimateGroup2
|
||||
};
|
||||
|
||||
return new PluginStateObject.Format.Json(
|
||||
{ kinData, visibilityState },
|
||||
{ label: a.label, description: a.description }
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const KinemageShapePointsProvider = Transform({
|
||||
name: 'sb-kinemage-shape-points-provider',
|
||||
display: { name: 'Kinemage Shape Points Provider' },
|
||||
@@ -282,13 +197,11 @@ export const KinemageShapePointsProvider = Transform({
|
||||
apply({ a }) {
|
||||
return Task.create('Kinemage Points Shape Provider', async ctx => {
|
||||
const kinData = (a.data as any).kinData as Kinemage;
|
||||
const visibilityState = (a.data as any).visibilityState as KinemageVisibilityState | undefined;
|
||||
|
||||
if (!kinData) {
|
||||
throw new Error('No kinData found in parent Format.Json node');
|
||||
}
|
||||
|
||||
const provider = await shapePointsFromKin(kinData, visibilityState, { transforms: undefined }, 'Dots').runInContext(ctx);
|
||||
const provider = await shapePointsFromKin(kinData, { transforms: undefined }, 'Dots').runInContext(ctx);
|
||||
return new PluginStateObject.Shape.Provider(provider as any, {
|
||||
label: kinData.pdbfile || kinData.caption || 'Kinemage Points',
|
||||
description: kinData.text || ''
|
||||
@@ -307,13 +220,11 @@ export const KinemageShapeLinesProvider = Transform({
|
||||
apply({ a }) {
|
||||
return Task.create('Kinemage Lines Shape Provider', async ctx => {
|
||||
const kinData = (a.data as any).kinData as Kinemage;
|
||||
const visibilityState = (a.data as any).visibilityState as KinemageVisibilityState | undefined;
|
||||
|
||||
if (!kinData) {
|
||||
throw new Error('No kinData found in parent Format.Json node');
|
||||
}
|
||||
|
||||
const provider = await shapeLinesFromKin(kinData, visibilityState).runInContext(ctx);
|
||||
const provider = await shapeLinesFromKin(kinData).runInContext(ctx);
|
||||
return new PluginStateObject.Shape.Provider(provider as any, {
|
||||
label: kinData.pdbfile || kinData.caption || 'Kinemage Lines',
|
||||
description: kinData.text || ''
|
||||
@@ -332,13 +243,11 @@ export const KinemageShapeMeshProvider = Transform({
|
||||
apply({ a }) {
|
||||
return Task.create('Kinemage Mesh Shape Provider', async ctx => {
|
||||
const kinData = (a.data as any).kinData as Kinemage;
|
||||
const visibilityState = (a.data as any).visibilityState as KinemageVisibilityState | undefined;
|
||||
|
||||
if (!kinData) {
|
||||
throw new Error('No kinData found in parent Format.Json node');
|
||||
}
|
||||
|
||||
const provider = await shapeMeshFromKin(kinData, visibilityState).runInContext(ctx);
|
||||
const provider = await shapeMeshFromKin(kinData).runInContext(ctx);
|
||||
return new PluginStateObject.Shape.Provider(provider as any, {
|
||||
label: kinData.pdbfile || kinData.caption || 'Kinemage Meshes',
|
||||
description: kinData.text || ''
|
||||
@@ -357,13 +266,11 @@ export const KinemageShapeSpheresProvider = Transform({
|
||||
apply({ a }) {
|
||||
return Task.create('Kinemage Spheres Shape Provider', async ctx => {
|
||||
const kinData = (a.data as any).kinData as Kinemage;
|
||||
const visibilityState = (a.data as any).visibilityState as KinemageVisibilityState | undefined;
|
||||
|
||||
if (!kinData) {
|
||||
throw new Error('No kinData found in parent Format.Json node');
|
||||
}
|
||||
|
||||
const provider = await shapeSpheresFromKin(kinData, visibilityState).runInContext(ctx);
|
||||
const provider = await shapeSpheresFromKin(kinData).runInContext(ctx);
|
||||
return new PluginStateObject.Shape.Provider(provider as any, {
|
||||
label: kinData.pdbfile || kinData.caption || 'Kinemage Spheres',
|
||||
description: kinData.text || ''
|
||||
@@ -439,40 +346,71 @@ interface DragAndDropHandler {
|
||||
}
|
||||
|
||||
/** Helper function to create all shapes for a kinemage via proper transform chain */
|
||||
async function createShapesForKinemage(plugin: PluginContext, update: StateBuilder.Root, visControllerSelector: StateObjectSelector<PluginStateObject.Format.Json>) {
|
||||
const visControllerCell = plugin.state.data.cells.get(visControllerSelector.ref);
|
||||
if (!visControllerCell?.obj?.data) return;
|
||||
async function createShapesForKinemage(plugin: PluginContext, update: StateBuilder.Root, kinDataSelector: StateObjectSelector<PluginStateObject.Format.Json>) {
|
||||
const kinDataCell = plugin.state.data.cells.get(kinDataSelector.ref);
|
||||
if (!kinDataCell?.obj?.data) return;
|
||||
|
||||
const kinData = (visControllerCell.obj.data as any).kinData as Kinemage;
|
||||
const kinData = (kinDataCell.obj.data as any).kinData as Kinemage;
|
||||
if (!kinData) return;
|
||||
|
||||
// Generate all shape types that have data, each as child of the visibility controller
|
||||
// Generate all shape types that have data, each as child of the selected kinemage
|
||||
if (kinData.dotLists.length > 0) {
|
||||
await update
|
||||
.to(visControllerSelector.ref)
|
||||
.to(kinDataSelector)
|
||||
.apply(KinemageShapePointsProvider, {}, { state: { isGhost: true } })
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
}
|
||||
if (kinData.vectorLists.length > 0) {
|
||||
await update
|
||||
.to(visControllerSelector.ref)
|
||||
.to(kinDataSelector)
|
||||
.apply(KinemageShapeLinesProvider, {}, { state: { isGhost: true } })
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
}
|
||||
if (kinData.ribbonLists.length > 0) {
|
||||
await update
|
||||
.to(visControllerSelector.ref)
|
||||
.to(kinDataSelector)
|
||||
.apply(KinemageShapeMeshProvider, {}, { state: { isGhost: true } })
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D, { doubleSided: true });
|
||||
}
|
||||
if (kinData.ballLists.length > 0) {
|
||||
await update
|
||||
.to(visControllerSelector.ref)
|
||||
.to(kinDataSelector)
|
||||
.apply(KinemageShapeSpheresProvider, {}, { state: { isGhost: true } })
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
}
|
||||
}
|
||||
|
||||
/** Helper function to rebuild shapes for a kinemage (remove and recreate) */
|
||||
export async function rebuildShapesForKinemage(plugin: PluginContext, kinDataSelector: StateObjectSelector<PluginStateObject.Format.Json>) {
|
||||
// Store current camera snapshot
|
||||
const curSnap = (plugin.canvas3d && (plugin.canvas3d as any).camera && (plugin.canvas3d as any).camera.getSnapshot)
|
||||
? (plugin.canvas3d as any).camera.getSnapshot()
|
||||
: undefined;
|
||||
|
||||
const update = plugin.state.data.build();
|
||||
|
||||
// Remove all children of this kinemage node (shapes/representations)
|
||||
const children = plugin.state.data.tree.children.get(kinDataSelector.ref);
|
||||
if (children) {
|
||||
for (const childRef of children.values()) {
|
||||
update.delete(childRef);
|
||||
}
|
||||
}
|
||||
|
||||
// Recreate shapes
|
||||
await createShapesForKinemage(plugin, update, kinDataSelector);
|
||||
await update.commit();
|
||||
|
||||
// Restore camera
|
||||
if (curSnap) {
|
||||
try {
|
||||
await applyViewSnapshot(plugin, curSnap);
|
||||
} catch (e) {
|
||||
console.warn('Failed to restore camera snapshot after recreating shapes', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Centralized helper to apply kinemage content into plugin state */
|
||||
async function applyKinemageToState(plugin: PluginContext, data: string, label?: string) {
|
||||
const update = plugin.state.data.build();
|
||||
@@ -486,42 +424,15 @@ async function applyKinemageToState(plugin: PluginContext, data: string, label?:
|
||||
const parsedNode = dataNode
|
||||
.apply(ParseKinemage, { label });
|
||||
|
||||
// Select first kinemage (default)
|
||||
const selectedNode = parsedNode
|
||||
.apply(SelectKinemage, { index: 0 });
|
||||
|
||||
await update.commit();
|
||||
|
||||
// Get the parsed kinemage object to see how many kinemages it contains
|
||||
const parsedCell = plugin.state.data.cells.get(parsedNode.ref);
|
||||
const kinemageData = parsedCell?.obj?.data as KinemageData | undefined;
|
||||
|
||||
if (!kinemageData || !kinemageData.kinemages || kinemageData.kinemages.length === 0) {
|
||||
console.warn('No kinemages found in parsed data');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Create a separate visibility controller and shapes for EACH kinemage
|
||||
const visControllerSelectors: StateObjectSelector<PluginStateObject.Format.Json>[] = [];
|
||||
|
||||
for (let i = 0; i < kinemageData.kinemages.length; i++) {
|
||||
const kinUpdate = plugin.state.data.build();
|
||||
|
||||
// Select this specific kinemage
|
||||
const selectedNode = kinUpdate
|
||||
.to(parsedNode.ref)
|
||||
.apply(SelectKinemage, { index: i });
|
||||
|
||||
// Add visibility controller for this kinemage
|
||||
const visControllerNode = selectedNode
|
||||
.apply(KinemageVisibilityController, {});
|
||||
|
||||
await kinUpdate.commit();
|
||||
|
||||
visControllerSelectors.push(visControllerNode.selector);
|
||||
}
|
||||
|
||||
// Now create shapes for all kinemages
|
||||
// Now create shapes from the selected kinemage
|
||||
const shapeUpdate = plugin.state.data.build();
|
||||
for (const visControllerSelector of visControllerSelectors) {
|
||||
await createShapesForKinemage(plugin, shapeUpdate, visControllerSelector);
|
||||
}
|
||||
await createShapesForKinemage(plugin, shapeUpdate, selectedNode.selector);
|
||||
await shapeUpdate.commit();
|
||||
|
||||
// Wait for bounding sphere and focus camera
|
||||
@@ -547,11 +458,11 @@ async function applyKinemageToState(plugin: PluginContext, data: string, label?:
|
||||
console.warn('Failed to apply initial kinemage view snapshot', e);
|
||||
}
|
||||
|
||||
return visControllerSelectors[0]; // Return first for backward compatibility
|
||||
return selectedNode.selector;
|
||||
}
|
||||
|
||||
/** Programmatic loader: load a single File (a .kin) into the plugin state.
|
||||
* Returns the ref to the first visibility controller node.
|
||||
* Returns the ref to the selected kinemage node.
|
||||
*/
|
||||
export async function loadKinemageFile(plugin: PluginContext, file: File): Promise<StateObjectSelector<PluginStateObject.Format.Json> | undefined> {
|
||||
const content = await file.text();
|
||||
@@ -586,53 +497,27 @@ const KINFormatProvider: DataFormatProvider<{}, any, any> = DataFormatProvider({
|
||||
.to(data)
|
||||
.apply(ParseKinemage, {});
|
||||
|
||||
const selectedKin = builder
|
||||
.apply(SelectKinemage, { index: 0 });
|
||||
|
||||
await builder.commit();
|
||||
|
||||
// Get the parsed data to see how many kinemages
|
||||
const parsedRef = builder.selector.ref;
|
||||
const parsedCell = plugin.state.data.cells.get(parsedRef);
|
||||
const kinemageData = parsedCell?.obj?.data as KinemageData | undefined;
|
||||
|
||||
if (!kinemageData || !kinemageData.kinemages || kinemageData.kinemages.length === 0) {
|
||||
console.warn('No kinemages found in parsed data');
|
||||
return {};
|
||||
}
|
||||
|
||||
// Create visibility controllers for all kinemages
|
||||
const visControllers: StateObjectSelector<PluginStateObject.Format.Json>[] = [];
|
||||
|
||||
for (let i = 0; i < kinemageData.kinemages.length; i++) {
|
||||
const kinBuilder = plugin.state.data.build();
|
||||
|
||||
const selectedKin = kinBuilder
|
||||
.to(parsedRef)
|
||||
.apply(SelectKinemage, { index: i });
|
||||
|
||||
const visController = selectedKin
|
||||
.apply(KinemageVisibilityController, {});
|
||||
|
||||
await kinBuilder.commit();
|
||||
visControllers.push(visController.selector);
|
||||
}
|
||||
|
||||
// Return all visibility controllers
|
||||
return { visControllers };
|
||||
// Return the selector for the selected kinemage so visuals can use it
|
||||
return { selectedKin: selectedKin.selector };
|
||||
} catch (e) {
|
||||
console.error('Failed to parse KIN file', e);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
visuals: async (plugin, data) => {
|
||||
if (!data?.visControllers || !Array.isArray(data.visControllers)) {
|
||||
console.warn('[Kinemage] visuals: no visControllers array provided');
|
||||
if (!data?.selectedKin) {
|
||||
console.warn('[Kinemage] visuals: no selectedKin ref provided');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create shapes for all kinemages
|
||||
// Create shapes from the selected kinemage
|
||||
const shapeBuilder = plugin.state.data.build();
|
||||
for (const visController of data.visControllers) {
|
||||
await createShapesForKinemage(plugin, shapeBuilder, visController);
|
||||
}
|
||||
await createShapesForKinemage(plugin, shapeBuilder, data.selectedKin);
|
||||
await shapeBuilder.commit();
|
||||
|
||||
// Wait for bounding sphere and focus camera
|
||||
|
||||
@@ -20,7 +20,6 @@ import { SpheresBuilder } from '../../mol-geo/geometry/spheres/spheres-builder';
|
||||
import { Shape } from '../../mol-model/shape';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
|
||||
import { KinemageVisibilityState } from './behavior';
|
||||
|
||||
export type KinData = {
|
||||
source: Kinemage,
|
||||
@@ -46,6 +45,9 @@ function createKinShapeMeshParams(kinemage?: Kinemage) {
|
||||
|
||||
return {
|
||||
...Mesh.Params,
|
||||
// transparentBackfaces: PD.Select('on', PD.arrayToOptions(['off', 'on', 'opaque'] as const)),
|
||||
// doubleSided: PD.Boolean(true), // make mesh double-sided by default
|
||||
// ignoreLight: PD.Boolean(true), // ignore lighting so front/back show same color
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,88 +64,46 @@ function createKinShapeSpheresParams(kinemage?: Kinemage) {
|
||||
export const KinShapeSpheresParams = createKinShapeSpheresParams();
|
||||
export type KinShapeSpheresParams = typeof KinShapeSpheresParams;
|
||||
|
||||
/**
|
||||
* Check visibility using AND logic:
|
||||
* - ALL masters must be visible
|
||||
* - AND group must be visible
|
||||
* - AND subgroup must be visible (and its parent group if it has one)
|
||||
*/
|
||||
function getVisibility(group: string, subGroup: string, masters: string[], kin: Kinemage, visibilityState?: KinemageVisibilityState) {
|
||||
// If no visibility state provided, fall back to checking the original parsed data
|
||||
if (!visibilityState) {
|
||||
let visible = true;
|
||||
function getVisibility(group: string, subGroup: string, masters: string[], kin: Kinemage) {
|
||||
let visible = true;
|
||||
|
||||
// Check masters from parsed data
|
||||
for (let m = 0; m < masters.length; m++) {
|
||||
const masterName = masters[m];
|
||||
const masterInfo = kin.masterDict[masterName];
|
||||
if (masterInfo && !masterInfo.visible) {
|
||||
visible = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check group from parsed data
|
||||
const groupInfo = kin.groupDict[group];
|
||||
if (groupInfo && (groupInfo as any).off) {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
// Check subgroup from parsed data
|
||||
const subgroupInfo = kin.subgroupDict[subGroup];
|
||||
if (subgroupInfo) {
|
||||
if ((subgroupInfo as any).off) {
|
||||
visible = false;
|
||||
}
|
||||
if ((subgroupInfo as any).group) {
|
||||
const parentGroupInfo = kin.groupDict[(subgroupInfo as any).group];
|
||||
if (parentGroupInfo && (parentGroupInfo as any).off) {
|
||||
visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return visible;
|
||||
}
|
||||
|
||||
// Use visibility state - all conditions must be true (AND logic)
|
||||
|
||||
// Check all masters - if ANY master is not visible, return false
|
||||
// Check to see if this name references a master that is not visible. If so, then this whole list is not visible and we can skip it.
|
||||
const masterDict = kin.masterDict;
|
||||
for (let m = 0; m < masters.length; m++) {
|
||||
const masterName = masters[m];
|
||||
const masterVisible = visibilityState.masterVisibility.get(masterName);
|
||||
if (masterVisible === false) {
|
||||
return false;
|
||||
const masterInfo = masterDict[masterName];
|
||||
if (masterInfo && !masterInfo.visible) {
|
||||
visible = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check group visibility
|
||||
const groupVisible = visibilityState.groupVisibility.get(group);
|
||||
if (groupVisible === false) {
|
||||
return false;
|
||||
// Check to see if this name references a group that has the 'off' flag set. If so, this is not visible.
|
||||
const groupDict = kin.groupDict;
|
||||
const groupInfo = groupDict[group];
|
||||
if (groupInfo && groupInfo.off) {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
// Check subgroup visibility
|
||||
if (subGroup) {
|
||||
const subgroupVisible = visibilityState.subgroupVisibility.get(subGroup);
|
||||
if (subgroupVisible === false) {
|
||||
return false;
|
||||
// Check to see if this name references a subgroup that it or its master has the 'off' flag set. If so, this is not visible.
|
||||
const subgroupDict = kin.subgroupDict;
|
||||
const subgroupInfo = subgroupDict[subGroup];
|
||||
if (subgroupInfo) {
|
||||
if (subgroupInfo.off) {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
// Also check if subgroup's parent group is visible
|
||||
const subgroupInfo = kin.subgroupDict[subGroup];
|
||||
if (subgroupInfo && (subgroupInfo as any).group) {
|
||||
const parentGroupVisible = visibilityState.groupVisibility.get((subgroupInfo as any).group);
|
||||
if (parentGroupVisible === false) {
|
||||
return false;
|
||||
if (subgroupInfo.group) {
|
||||
const groupInfo = groupDict[subgroupInfo.group];
|
||||
if (groupInfo && groupInfo.off) {
|
||||
visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return visible;
|
||||
}
|
||||
|
||||
async function getPoints(ctx: RuntimeContext, kin: Kinemage, visibilityState?: KinemageVisibilityState) {
|
||||
async function getPoints(ctx: RuntimeContext, kin: Kinemage) {
|
||||
const dotLists: DotList[] = kin.dotLists;
|
||||
const builderState = PointsBuilder.create();
|
||||
const colors: Color[] = [];
|
||||
@@ -160,7 +120,7 @@ async function getPoints(ctx: RuntimeContext, kin: Kinemage, visibilityState?: K
|
||||
const masterArray = dotList.masterArray;
|
||||
|
||||
// Check the visibility of all of our masters and skip this dot list if any of them are not visible.
|
||||
const visible = getVisibility(dotList.group, dotList.subgroup, masterArray, kin, visibilityState);
|
||||
const visible = getVisibility(dotList.group, dotList.subgroup, masterArray, kin);
|
||||
if (!visible) { continue; }
|
||||
|
||||
const numDots = positionArray.length / 3;
|
||||
@@ -171,18 +131,10 @@ async function getPoints(ctx: RuntimeContext, kin: Kinemage, visibilityState?: K
|
||||
for (let pm = 0; pm < pointMasterNames.length; pm++) {
|
||||
const pointMasterName = pointMasterNames[pm];
|
||||
const masterName = kin.pointmasterDict[pointMasterName];
|
||||
if (visibilityState) {
|
||||
const masterVisible = visibilityState.masterVisibility.get(masterName);
|
||||
if (masterVisible === false) {
|
||||
pmVisibility = false;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
const masterInfo = kin.masterDict[masterName];
|
||||
if (masterInfo && !masterInfo.visible) {
|
||||
pmVisibility = false;
|
||||
break;
|
||||
}
|
||||
const masterInfo = kin.masterDict[masterName];
|
||||
if (masterInfo && !masterInfo.visible) {
|
||||
pmVisibility = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!pmVisibility) { continue; }
|
||||
@@ -200,7 +152,7 @@ async function getPoints(ctx: RuntimeContext, kin: Kinemage, visibilityState?: K
|
||||
return { points, colors, labels };
|
||||
}
|
||||
|
||||
async function getLines(ctx: RuntimeContext, kin: Kinemage, visibilityState?: KinemageVisibilityState) {
|
||||
async function getLines(ctx: RuntimeContext, kin: Kinemage) {
|
||||
const vectorLists: VectorList[] = kin.vectorLists;
|
||||
const builderState = LinesBuilder.create();
|
||||
const widths: number[] = [];
|
||||
@@ -223,7 +175,7 @@ async function getLines(ctx: RuntimeContext, kin: Kinemage, visibilityState?: Ki
|
||||
const masterArray = vectorList.masterArray;
|
||||
|
||||
// Check the visibility of all of our masters and skip this vector list if any of them are not visible.
|
||||
const visible = getVisibility(vectorList.group, vectorList.subgroup, masterArray, kin, visibilityState);
|
||||
const visible = getVisibility(vectorList.group, vectorList.subgroup, masterArray, kin);
|
||||
if (!visible) { continue; }
|
||||
|
||||
const numLines = position1Array.length / 3;
|
||||
@@ -234,18 +186,10 @@ async function getLines(ctx: RuntimeContext, kin: Kinemage, visibilityState?: Ki
|
||||
for (let pm = 0; pm < pointMasterNames.length; pm++) {
|
||||
const pointMasterName = pointMasterNames[pm];
|
||||
const masterName = kin.pointmasterDict[pointMasterName];
|
||||
if (visibilityState) {
|
||||
const masterVisible = visibilityState.masterVisibility.get(masterName);
|
||||
if (masterVisible === false) {
|
||||
pmVisibility = false;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
const masterInfo = kin.masterDict[masterName];
|
||||
if (masterInfo && !masterInfo.visible) {
|
||||
pmVisibility = false;
|
||||
break;
|
||||
}
|
||||
const masterInfo = kin.masterDict[masterName];
|
||||
if (masterInfo && !masterInfo.visible) {
|
||||
pmVisibility = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!pmVisibility) { continue; }
|
||||
@@ -293,7 +237,7 @@ function addOffsetTriangle(builderState: MeshBuilder.State, a: Vec3, b: Vec3, c:
|
||||
MeshBuilder.addTriangleWithNormal(builderState, aOffset, bOffset, cOffset, n);
|
||||
}
|
||||
|
||||
async function getMesh(ctx: RuntimeContext, kin: Kinemage, visibilityState?: KinemageVisibilityState) {
|
||||
async function getMesh(ctx: RuntimeContext, kin: Kinemage) {
|
||||
const ribbonObjects: RibbonObject[] = kin.ribbonLists;
|
||||
const builderState = MeshBuilder.createState();
|
||||
const colors: Color[] = [];
|
||||
@@ -312,7 +256,7 @@ async function getMesh(ctx: RuntimeContext, kin: Kinemage, visibilityState?: Kin
|
||||
const pointMasterArray = ribbonObject.pointmasterArray;
|
||||
|
||||
// Check the visibility of all of our masters and skip this ribbon object if any of them are not visible.
|
||||
const visible = getVisibility(ribbonObject.group, ribbonObject.subgroup, masterArray, kin, visibilityState);
|
||||
const visible = getVisibility(ribbonObject.group, ribbonObject.subgroup, masterArray, kin);
|
||||
if (!visible) { continue; }
|
||||
|
||||
builderState.currentGroup = ri; // TODO: Base this on something in the file instead?
|
||||
@@ -329,18 +273,10 @@ async function getMesh(ctx: RuntimeContext, kin: Kinemage, visibilityState?: Kin
|
||||
for (let pm = 0; pm < pointMasterNames.length; pm++) {
|
||||
const pointMasterName = pointMasterNames[pm];
|
||||
const masterName = kin.pointmasterDict[pointMasterName];
|
||||
if (visibilityState) {
|
||||
const masterVisible = visibilityState.masterVisibility.get(masterName);
|
||||
if (masterVisible === false) {
|
||||
pmVisibility = false;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
const masterInfo = kin.masterDict[masterName];
|
||||
if (masterInfo && !masterInfo.visible) {
|
||||
pmVisibility = false;
|
||||
break;
|
||||
}
|
||||
const masterInfo = kin.masterDict[masterName];
|
||||
if (masterInfo && !masterInfo.visible) {
|
||||
pmVisibility = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!pmVisibility) { continue; }
|
||||
@@ -400,7 +336,7 @@ async function getMesh(ctx: RuntimeContext, kin: Kinemage, visibilityState?: Kin
|
||||
* Build spheres geometry and collect per-sphere radii from the KIN BallList entries.
|
||||
* Returns an object with the Spheres geometry and a Float32Array with per-center radii (one entry per center, in the same order they were added).
|
||||
*/
|
||||
async function getSpheres(ctx: RuntimeContext, kin: Kinemage, visibilityState?: KinemageVisibilityState) {
|
||||
async function getSpheres(ctx: RuntimeContext, kin: Kinemage) {
|
||||
const balls: BallList[] = kin.ballLists;
|
||||
const builderState = SpheresBuilder.create();
|
||||
const radii: number[] = [];
|
||||
@@ -419,7 +355,7 @@ async function getSpheres(ctx: RuntimeContext, kin: Kinemage, visibilityState?:
|
||||
const masterArray = ballList.masterArray;
|
||||
|
||||
// Check the visibility of all of our masters and skip this ball list if any of them are not visible.
|
||||
const visible = getVisibility(ballList.group, ballList.subgroup, masterArray, kin, visibilityState);
|
||||
const visible = getVisibility(ballList.group, ballList.subgroup, masterArray, kin);
|
||||
if (!visible) { continue; }
|
||||
|
||||
const numBalls = positionArray.length / 3;
|
||||
@@ -430,18 +366,10 @@ async function getSpheres(ctx: RuntimeContext, kin: Kinemage, visibilityState?:
|
||||
for (let pm = 0; pm < pointMasterNames.length; pm++) {
|
||||
const pointMasterName = pointMasterNames[pm];
|
||||
const masterName = kin.pointmasterDict[pointMasterName];
|
||||
if (visibilityState) {
|
||||
const masterVisible = visibilityState.masterVisibility.get(masterName);
|
||||
if (masterVisible === false) {
|
||||
pmVisibility = false;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
const masterInfo = kin.masterDict[masterName];
|
||||
if (masterInfo && !masterInfo.visible) {
|
||||
pmVisibility = false;
|
||||
break;
|
||||
}
|
||||
const masterInfo = kin.masterDict[masterName];
|
||||
if (masterInfo && !masterInfo.visible) {
|
||||
pmVisibility = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!pmVisibility) { continue; }
|
||||
@@ -461,11 +389,11 @@ async function getSpheres(ctx: RuntimeContext, kin: Kinemage, visibilityState?:
|
||||
return { spheres, radii: new Float32Array(radii), colors, labels };
|
||||
}
|
||||
|
||||
function makePointsShapeGetter(visibilityState?: KinemageVisibilityState) {
|
||||
function makePointsShapeGetter() {
|
||||
|
||||
const getShape = async (ctx: RuntimeContext, kinData: KinData, props: PD.Values<KinShapePointsParams>, shape?: Shape<Points>) => {
|
||||
// Get our points, adding them from all of the entries in the dot lists
|
||||
const { points: _points, colors, labels } = await getPoints(ctx, kinData.source, visibilityState);
|
||||
const { points: _points, colors, labels } = await getPoints(ctx, kinData.source);
|
||||
|
||||
// Color function signature: (groupId: number, instanceId: number) => Color
|
||||
// For Lines the groupId corresponds to the line index (order added).
|
||||
@@ -492,11 +420,11 @@ function makePointsShapeGetter(visibilityState?: KinemageVisibilityState) {
|
||||
return getShape;
|
||||
}
|
||||
|
||||
function makeLineShapeGetter(visibilityState?: KinemageVisibilityState) {
|
||||
function makeLineShapeGetter() {
|
||||
|
||||
const getShape = async (ctx: RuntimeContext, kinData: KinData, props: PD.Values<KinShapeLinesParams>, shape?: Shape<Lines>) => {
|
||||
// Get our lines, adding them from all of the entries in the vector lists
|
||||
const { lines: _lines, widths, colors, labels } = await getLines(ctx, kinData.source, visibilityState);
|
||||
const { lines: _lines, widths, colors, labels } = await getLines(ctx, kinData.source);
|
||||
|
||||
// Size function signature: (groupId: number, instanceId: number) => number
|
||||
// For Lines the groupId corresponds to the line index (order added).
|
||||
@@ -532,11 +460,11 @@ function makeLineShapeGetter(visibilityState?: KinemageVisibilityState) {
|
||||
return getShape;
|
||||
}
|
||||
|
||||
function makeMeshShapeGetter(visibilityState?: KinemageVisibilityState) {
|
||||
function makeMeshShapeGetter() {
|
||||
|
||||
const getShape = async (ctx: RuntimeContext, kinData: KinData, props: PD.Values<KinShapeMeshParams>, shape?: Shape<Mesh>) => {
|
||||
|
||||
let { mesh: _mesh, colors, labels } = await getMesh(ctx, kinData.source, visibilityState);
|
||||
let { mesh: _mesh, colors, labels } = await getMesh(ctx, kinData.source);
|
||||
// Ensure that _mesh is not undifined before we pass it to Shape.create. If it is undefined, create an empty mesh instead.
|
||||
if (!_mesh) {
|
||||
console.warn('No mesh could be created from the KIN data. Creating an empty mesh instead.');
|
||||
@@ -569,11 +497,11 @@ function makeMeshShapeGetter(visibilityState?: KinemageVisibilityState) {
|
||||
/**
|
||||
* Spheres shape getter: uses per-center radii read from the KIN BallList radiusArray when available.
|
||||
*/
|
||||
function makeSpheresShapeGetter(visibilityState?: KinemageVisibilityState) {
|
||||
function makeSpheresShapeGetter() {
|
||||
|
||||
const getShape = async (ctx: RuntimeContext, kinData: KinData, props: PD.Values<KinShapeSpheresParams>, shape?: Shape<Spheres>) => {
|
||||
// Build spheres geometry and collect per-center radii
|
||||
const { spheres: _spheres, radii, colors, labels } = await getSpheres(ctx, kinData.source, visibilityState);
|
||||
const { spheres: _spheres, radii, colors, labels } = await getSpheres(ctx, kinData.source);
|
||||
|
||||
// size function signature: (groupId: number, instanceId: number) => number
|
||||
// For Spheres the groupId corresponds to the center index (order added).
|
||||
@@ -607,49 +535,49 @@ function makeSpheresShapeGetter(visibilityState?: KinemageVisibilityState) {
|
||||
return getShape;
|
||||
}
|
||||
|
||||
export function shapePointsFromKin(source: Kinemage, visibilityState: KinemageVisibilityState | undefined, params?: { transforms?: Mat4[] }, label?: string) {
|
||||
export function shapePointsFromKin(source: Kinemage, params?: { transforms?: Mat4[] }, label?: string) {
|
||||
return Task.create<ShapeProvider<KinData, Points, KinShapePointsParams>>('Kin Shape Points Provider', async ctx => {
|
||||
return {
|
||||
label: label ?? 'Points',
|
||||
data: { source, transforms: params?.transforms },
|
||||
params: createKinShapePointsParams(source),
|
||||
getShape: makePointsShapeGetter(visibilityState),
|
||||
getShape: makePointsShapeGetter(),
|
||||
geometryUtils: Points.Utils
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function shapeLinesFromKin(source: Kinemage, visibilityState: KinemageVisibilityState | undefined, params?: { transforms?: Mat4[] }, label?: string) {
|
||||
export function shapeLinesFromKin(source: Kinemage, params?: { transforms?: Mat4[] }, label?: string) {
|
||||
return Task.create<ShapeProvider<KinData, Lines, KinShapeLinesParams>>('Kin Shape Lines Provider', async ctx => {
|
||||
return {
|
||||
label: label ?? 'Lines',
|
||||
data: { source, transforms: params?.transforms },
|
||||
params: createKinShapeLinesParams(source),
|
||||
getShape: makeLineShapeGetter(visibilityState),
|
||||
getShape: makeLineShapeGetter(),
|
||||
geometryUtils: Lines.Utils
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function shapeMeshFromKin(source: Kinemage, visibilityState: KinemageVisibilityState | undefined, params?: { transforms?: Mat4[] }, label?: string) {
|
||||
export function shapeMeshFromKin(source: Kinemage, params?: { transforms?: Mat4[] }, label?: string) {
|
||||
return Task.create<ShapeProvider<KinData, Mesh, KinShapeMeshParams>>('Kin Shape Mesh Provider', async ctx => {
|
||||
return {
|
||||
label: label ?? 'Meshes',
|
||||
data: { source, transforms: params?.transforms },
|
||||
params: createKinShapeMeshParams(source),
|
||||
getShape: makeMeshShapeGetter(visibilityState),
|
||||
getShape: makeMeshShapeGetter(),
|
||||
geometryUtils: Mesh.Utils
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function shapeSpheresFromKin(source: Kinemage, visibilityState: KinemageVisibilityState | undefined, params?: { transforms?: Mat4[] }, label?: string) {
|
||||
export function shapeSpheresFromKin(source: Kinemage, params?: { transforms?: Mat4[] }, label?: string) {
|
||||
return Task.create<ShapeProvider<KinData, Spheres, KinShapeSpheresParams>>('Kin Shape Spheres Provider', async ctx => {
|
||||
return {
|
||||
label: label ?? 'Spheres',
|
||||
data: { source, transforms: params?.transforms },
|
||||
params: createKinShapeSpheresParams(source),
|
||||
getShape: makeSpheresShapeGetter(visibilityState),
|
||||
getShape: makeSpheresShapeGetter(),
|
||||
geometryUtils: Spheres.Utils
|
||||
};
|
||||
});
|
||||
|
||||
@@ -322,6 +322,11 @@ function removePointBreaksTriangleArrays(convertedRibbonObject: RibbonObject) {
|
||||
editedPMs.push(convertedRibbonObject.pointmasterArray[breakPointer]);
|
||||
editedPMs.push(convertedRibbonObject.pointmasterArray[breakPointer + 1]);
|
||||
editedPMs.push(convertedRibbonObject.pointmasterArray[breakPointer + 2]);
|
||||
} else {
|
||||
// console.log('X triangle break found')
|
||||
// console.log('skipping: '+positionArray[positionPointer]+','+positionArray[positionPointer+1]+','+positionArray[positionPointer+2]+','
|
||||
// +positionArray[positionPointer+3]+','+positionArray[positionPointer+4]+','+positionArray[positionPointer+5]+','
|
||||
// +positionArray[positionPointer+6]+','+positionArray[positionPointer+7]+','+positionArray[positionPointer+8])
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -8,422 +8,298 @@
|
||||
* Kinemage right-panel controls (right-panel only).
|
||||
*
|
||||
* Shows kinemage views, animate buttons, and group/subgroup/master toggles in the right inspector.
|
||||
* Controls update visibility controller parameters which trigger rebuilds via the state tree.
|
||||
* Controls directly operate on the loaded kinemage runtime data and call exported helpers
|
||||
* to rebuild visuals. No State Tree JSON nodes are created for these UI items.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { CollapsableState, CollapsableControls } from '../../mol-plugin-ui/base';
|
||||
import { Camera } from '../../mol-canvas3d/camera';
|
||||
import { applyViewSnapshot } from './behavior';
|
||||
import { applyViewSnapshot, rebuildShapesForKinemage } from './behavior';
|
||||
import { Kinemage } from './reader/schema';
|
||||
import { StateTransforms } from '../../mol-plugin-state/transforms';
|
||||
import { KinemageShapePointsProvider, KinemageShapeLinesProvider, KinemageShapeMeshProvider, KinemageShapeSpheresProvider } from './behavior';
|
||||
|
||||
interface KinemageControlState extends CollapsableState {
|
||||
isBusy: boolean
|
||||
isBusy: boolean
|
||||
}
|
||||
|
||||
function nameFromString(s: string | undefined) {
|
||||
// If this is undefined, return undefined.
|
||||
if (!s) return undefined;
|
||||
// Return up to the first 30 characters of the string.
|
||||
return s.length > 30 ? s.substring(0, 30) + '...' : s;
|
||||
// If this is undefined, return undefined.
|
||||
if (!s) return undefined;
|
||||
// Return up to the first 30 characters of the string.
|
||||
return s.length > 30 ? s.substring(0, 30) + '...' : s;
|
||||
}
|
||||
|
||||
export class KinemageControls extends CollapsableControls<{}, KinemageControlState> {
|
||||
protected defaultState(): KinemageControlState {
|
||||
return {
|
||||
header: 'Kinemage',
|
||||
isCollapsed: false,
|
||||
isBusy: false,
|
||||
// default hidden until a kinemage is present
|
||||
isHidden: true,
|
||||
brand: { accent: 'cyan', svg: undefined as any }
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Listen for shape/state changes: when state tree cells are created or removed the visuals changed.
|
||||
this.subscribe(this.plugin.state.data.events.cell.created, (e: any) => this.onCellCreated(e));
|
||||
this.subscribe(this.plugin.state.data.events.cell.removed, () => this.onCellRemoved());
|
||||
// also track cell state updates that may change labels / visibility
|
||||
this.subscribe(this.plugin.state.data.events.cell.stateUpdated, () => this.forceUpdate());
|
||||
|
||||
// ensure initial visibility reflects current state
|
||||
this.updateVisibility();
|
||||
}
|
||||
|
||||
private onCellCreated(e: any) {
|
||||
this.updateVisibility();
|
||||
}
|
||||
|
||||
private onCellRemoved() {
|
||||
this.updateVisibility();
|
||||
}
|
||||
|
||||
private updateVisibility() {
|
||||
const kinemages = this.getKinemageList();
|
||||
this.setState({ isHidden: kinemages.length === 0 });
|
||||
}
|
||||
|
||||
private getKinemageList(): Array<{ kinData: Kinemage, ref: string, visControllerRef: string }> {
|
||||
const result: Array<{ kinData: Kinemage, ref: string, visControllerRef: string }> = [];
|
||||
|
||||
try {
|
||||
const cells = (this.plugin.state.data as any).cells as Map<string, any>;
|
||||
for (const [ref, entry] of cells) {
|
||||
const obj = (entry as any).obj;
|
||||
// Look for Format.Json nodes that contain kinData and visibilityState (visibility controller)
|
||||
if (obj && obj.data && (obj.data as any).kinData && (obj.data as any).visibilityState) {
|
||||
result.push({
|
||||
kinData: (obj.data as any).kinData,
|
||||
ref,
|
||||
visControllerRef: ref
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to enumerate kinemage nodes', e);
|
||||
protected defaultState(): KinemageControlState {
|
||||
return {
|
||||
header: 'Kinemage',
|
||||
isCollapsed: false,
|
||||
isBusy: false,
|
||||
// default hidden until a kinemage is present
|
||||
isHidden: true,
|
||||
brand: { accent: 'cyan', svg: undefined as any }
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
componentDidMount() {
|
||||
// Listen for shape/state changes: when state tree cells are created or removed the visuals changed.
|
||||
this.subscribe(this.plugin.state.data.events.cell.created, (e: any) => this.onCellCreated(e));
|
||||
this.subscribe(this.plugin.state.data.events.cell.removed, () => this.onCellRemoved());
|
||||
// also track cell state updates that may change labels / visibility
|
||||
this.subscribe(this.plugin.state.data.events.cell.stateUpdated, () => this.forceUpdate());
|
||||
|
||||
private getAllDescendants(nodeRef: string): string[] {
|
||||
const result: string[] = [];
|
||||
const tree = this.plugin.state.data.tree;
|
||||
const queue = [nodeRef];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!;
|
||||
const children = tree.children.get(current);
|
||||
if (children) {
|
||||
for (const childRef of children.values()) {
|
||||
result.push(childRef);
|
||||
queue.push(childRef);
|
||||
}
|
||||
}
|
||||
// ensure initial visibility reflects current state
|
||||
this.updateVisibility();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async applyView(kinData: Kinemage, viewKey: string) {
|
||||
const snap = (kinData as any).viewSnapshots?.[viewKey];
|
||||
if (snap) {
|
||||
await applyViewSnapshot(this.plugin, snap as Partial<Camera.Snapshot>);
|
||||
}
|
||||
}
|
||||
|
||||
private async rebuildShapes(visControllerRef: string, kinData: Kinemage) {
|
||||
const update = this.plugin.state.data.build();
|
||||
|
||||
// Delete all descendants (shape providers and representations)
|
||||
const descendants = this.getAllDescendants(visControllerRef);
|
||||
for (const nodeRef of descendants) {
|
||||
update.delete(nodeRef);
|
||||
private onCellCreated(e: any) {
|
||||
this.updateVisibility();
|
||||
}
|
||||
|
||||
await update.commit();
|
||||
|
||||
// Recreate shapes
|
||||
const rebuildUpdate = this.plugin.state.data.build();
|
||||
|
||||
// Generate all shape types that have data, each as child of the visibility controller
|
||||
if (kinData.dotLists.length > 0) {
|
||||
rebuildUpdate
|
||||
.to(visControllerRef)
|
||||
.apply(KinemageShapePointsProvider, {}, { state: { isGhost: true } })
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
}
|
||||
if (kinData.vectorLists.length > 0) {
|
||||
rebuildUpdate
|
||||
.to(visControllerRef)
|
||||
.apply(KinemageShapeLinesProvider, {}, { state: { isGhost: true } })
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
}
|
||||
if (kinData.ribbonLists.length > 0) {
|
||||
rebuildUpdate
|
||||
.to(visControllerRef)
|
||||
.apply(KinemageShapeMeshProvider, {}, { state: { isGhost: true } })
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D, { doubleSided: true });
|
||||
}
|
||||
if (kinData.ballLists.length > 0) {
|
||||
rebuildUpdate
|
||||
.to(visControllerRef)
|
||||
.apply(KinemageShapeSpheresProvider, {}, { state: { isGhost: true } })
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
private onCellRemoved() {
|
||||
this.updateVisibility();
|
||||
}
|
||||
|
||||
await rebuildUpdate.commit();
|
||||
}
|
||||
|
||||
private async toggleVisibility(visControllerRef: string, kinData: Kinemage, target: { type: 'group' | 'subgroup' | 'master', key: string }) {
|
||||
try {
|
||||
const cell = this.plugin.state.data.cells.get(visControllerRef);
|
||||
if (!cell || !cell.transform || !cell.transform.params) return;
|
||||
|
||||
const currentParams = cell.transform.params;
|
||||
const newGroupVisibility = { ...currentParams.groupVisibility };
|
||||
const newSubgroupVisibility = { ...currentParams.subgroupVisibility };
|
||||
const newMasterVisibility = { ...currentParams.masterVisibility };
|
||||
|
||||
if (target.type === 'group') {
|
||||
newGroupVisibility[target.key] = !newGroupVisibility[target.key];
|
||||
} else if (target.type === 'subgroup') {
|
||||
newSubgroupVisibility[target.key] = !newSubgroupVisibility[target.key];
|
||||
} else {
|
||||
newMasterVisibility[target.key] = !newMasterVisibility[target.key];
|
||||
}
|
||||
|
||||
const update = this.plugin.state.data.build();
|
||||
|
||||
// Update the visibility controller
|
||||
update.to(visControllerRef).update({
|
||||
groupVisibility: newGroupVisibility,
|
||||
subgroupVisibility: newSubgroupVisibility,
|
||||
masterVisibility: newMasterVisibility
|
||||
});
|
||||
|
||||
await update.commit();
|
||||
|
||||
// Rebuild all shapes to reflect new visibility
|
||||
await this.rebuildShapes(visControllerRef, kinData);
|
||||
} catch (e) {
|
||||
console.error('Failed to toggle kinemage visibility', e);
|
||||
private updateVisibility() {
|
||||
const kinemages = this.getKinemageList();
|
||||
this.setState({ isHidden: kinemages.length === 0 });
|
||||
}
|
||||
}
|
||||
|
||||
private async triggerAnimateForKin(visControllerRef: string, kinData: Kinemage, mode: 'animate' | '2animate') {
|
||||
try {
|
||||
const cell = this.plugin.state.data.cells.get(visControllerRef);
|
||||
if (!cell || !cell.transform || !cell.transform.params) return;
|
||||
private getKinemageList(): Array<{ kinData: Kinemage, ref: string }> {
|
||||
const result: Array<{ kinData: Kinemage, ref: string }> = [];
|
||||
|
||||
const currentParams = cell.transform.params;
|
||||
const animateGroups = mode === 'animate' ? kinData.groupsAnimate : kinData.groupsAnimate2;
|
||||
const currentActive = mode === 'animate' ? currentParams.activeAnimateGroup : currentParams.activeAnimateGroup2;
|
||||
const nextActive = (currentActive + 1) % Math.max(1, animateGroups.length);
|
||||
|
||||
// IMPORTANT: Read the CURRENT visibility state from the controller node's data (not params)
|
||||
// to preserve any changes made through UI interactions
|
||||
const controllerCell = this.plugin.state.data.cells.get(visControllerRef);
|
||||
const currentVisibilityState = controllerCell?.obj?.data ? (controllerCell.obj.data as any).visibilityState : null;
|
||||
|
||||
// Start with current actual visibility state
|
||||
const newGroupVisibility = currentVisibilityState
|
||||
? Object.fromEntries(currentVisibilityState.groupVisibility)
|
||||
: { ...currentParams.groupVisibility };
|
||||
|
||||
// Only update the animate groups - leave everything else as-is
|
||||
for (let i = 0; i < animateGroups.length; i++) {
|
||||
newGroupVisibility[animateGroups[i]] = (i === nextActive);
|
||||
}
|
||||
|
||||
const update = this.plugin.state.data.build();
|
||||
|
||||
// Update the visibility controller with current visibility PLUS animate changes
|
||||
const updateParams: any = {
|
||||
groupVisibility: newGroupVisibility,
|
||||
};
|
||||
|
||||
if (mode === 'animate') {
|
||||
updateParams.activeAnimateGroup = nextActive;
|
||||
} else {
|
||||
updateParams.activeAnimateGroup2 = nextActive;
|
||||
}
|
||||
|
||||
// Also preserve other visibility states
|
||||
if (currentVisibilityState) {
|
||||
updateParams.subgroupVisibility = Object.fromEntries(currentVisibilityState.subgroupVisibility);
|
||||
updateParams.masterVisibility = Object.fromEntries(currentVisibilityState.masterVisibility);
|
||||
} else {
|
||||
updateParams.subgroupVisibility = currentParams.subgroupVisibility;
|
||||
updateParams.masterVisibility = currentParams.masterVisibility;
|
||||
}
|
||||
|
||||
update.to(visControllerRef).update(updateParams);
|
||||
|
||||
await update.commit();
|
||||
|
||||
// Rebuild all shapes to reflect new visibility
|
||||
await this.rebuildShapes(visControllerRef, kinData);
|
||||
} catch (e) {
|
||||
console.error('Failed to trigger animate', e);
|
||||
}
|
||||
}
|
||||
|
||||
private isVisible(visControllerRef: string, target: { type: 'group' | 'subgroup' | 'master', key: string }): boolean {
|
||||
try {
|
||||
const cell = this.plugin.state.data.cells.get(visControllerRef);
|
||||
if (!cell || !cell.transform || !cell.transform.params) return true;
|
||||
|
||||
const params = cell.transform.params;
|
||||
if (target.type === 'group') {
|
||||
return params.groupVisibility[target.key] !== false;
|
||||
} else if (target.type === 'subgroup') {
|
||||
return params.subgroupVisibility[target.key] !== false;
|
||||
} else {
|
||||
return params.masterVisibility[target.key] !== false;
|
||||
}
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
renderControls() {
|
||||
const kins = this.getKinemageList();
|
||||
if (kins.length === 0) return <div style={{ padding: '6px' }}>No Kinemage data</div>;
|
||||
|
||||
const blocks: React.ReactNode[] = [];
|
||||
for (const { kinData, visControllerRef } of kins) {
|
||||
const title = kinData.pdbfile || nameFromString(kinData.caption) || 'Kinemage';
|
||||
const kinBlock: React.ReactNode[] = [];
|
||||
|
||||
// Title
|
||||
kinBlock.push(
|
||||
<div key={'title-' + title} style={{ padding: '6px', fontWeight: 'bold', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
|
||||
// views
|
||||
const viewEntries = Object.entries(kinData.viewDict || {});
|
||||
if (viewEntries.length > 0) {
|
||||
for (const [viewKey, viewObj] of viewEntries) {
|
||||
const label = `View ${viewObj.name || `View ${viewKey}`}`;
|
||||
kinBlock.push(
|
||||
<div key={'view-' + title + '-' + viewKey} style={{ padding: '2px 6px' }}>
|
||||
<button
|
||||
className='msp-btn msp-btn-block'
|
||||
onClick={() => this.applyView(kinData, viewKey)}
|
||||
title={`Apply view: ${label}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// animate
|
||||
if (kinData.groupsAnimate && kinData.groupsAnimate.length > 0) {
|
||||
kinBlock.push(
|
||||
<div key={'anim-' + title} style={{ padding: '2px 6px' }}>
|
||||
<button
|
||||
className='msp-btn msp-btn-block'
|
||||
onClick={() => this.triggerAnimateForKin(visControllerRef, kinData, 'animate')}
|
||||
title='Cycle through animation frames'
|
||||
>
|
||||
Animate
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (kinData.groupsAnimate2 && kinData.groupsAnimate2.length > 0) {
|
||||
kinBlock.push(
|
||||
<div key={'anim2-' + title} style={{ padding: '2px 6px' }}>
|
||||
<button
|
||||
className='msp-btn msp-btn-block'
|
||||
onClick={() => this.triggerAnimateForKin(visControllerRef, kinData, '2animate')}
|
||||
title='Cycle through second animation frames'
|
||||
>
|
||||
Animate2
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// groups
|
||||
for (const [groupKey, groupInfo] of Object.entries(kinData.groupDict || {})) {
|
||||
if (!(groupInfo as any).nobutton) {
|
||||
const visible = this.isVisible(visControllerRef, { type: 'group', key: groupKey });
|
||||
// If this group is in animate or animate2, then add '*' before its groupKey name to indicate that it's an animation group
|
||||
const isAnimate = (kinData.groupsAnimate?.includes(groupKey) ?? false) || (kinData.groupsAnimate2?.includes(groupKey) ?? false);
|
||||
const label = isAnimate ? `* ${groupKey}` : groupKey;
|
||||
kinBlock.push(
|
||||
<div key={'group-' + title + '-' + groupKey} style={{ padding: '2px 6px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={visible}
|
||||
onChange={() => this.toggleVisibility(visControllerRef, kinData, { type: 'group', key: groupKey })}
|
||||
style={{ marginRight: '6px' }}
|
||||
/>
|
||||
<span title={label}>{label}</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// If this group is not dominant, find any subgroups of this group and show them here (indented) unless they have nobutton set
|
||||
if (!(groupInfo as any).dominant) {
|
||||
for (const [subgroupKey, subgroupInfo] of Object.entries(kinData.subgroupDict || {})) {
|
||||
if (subgroupKey.startsWith(groupKey + ':')) {
|
||||
if ((subgroupInfo as any).nobutton) continue;
|
||||
const visible = this.isVisible(visControllerRef, { type: 'subgroup', key: subgroupKey });
|
||||
const subgroupLabel = subgroupKey.split(':')[1];
|
||||
kinBlock.push(
|
||||
<div key={'subgroup-' + title + '-' + subgroupKey} style={{ padding: '2px 6px', paddingLeft: '24px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={visible}
|
||||
onChange={() => this.toggleVisibility(visControllerRef, kinData, { type: 'subgroup', key: subgroupKey })}
|
||||
style={{ marginRight: '6px' }}
|
||||
/>
|
||||
<span title={subgroupLabel}>{subgroupLabel}</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
try {
|
||||
const cells = (this.plugin.state.data as any).cells as Map<string, any>;
|
||||
for (const [ref, entry] of cells) {
|
||||
const obj = (entry as any).obj;
|
||||
// Look for Format.Json nodes that contain kinData
|
||||
if (obj && obj.data && (obj.data as any).kinData) {
|
||||
result.push({ kinData: (obj.data as any).kinData, ref });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to enumerate kinemage nodes', e);
|
||||
}
|
||||
}
|
||||
|
||||
// subgroups that don't belong to a group (standalone)
|
||||
for (const [subgroupKey, subgroupInfo] of Object.entries(kinData.subgroupDict || {})) {
|
||||
// if parent group present, those groups' subgroups are already shown when iterating groups
|
||||
if (subgroupKey.indexOf(':') !== -1) {
|
||||
// subgroups with parent group; skip here (shown under parent group)
|
||||
continue;
|
||||
}
|
||||
if ((subgroupInfo as any).nobutton) continue;
|
||||
const visible = this.isVisible(visControllerRef, { type: 'subgroup', key: subgroupKey });
|
||||
kinBlock.push(
|
||||
<div key={'subgroup-' + title + '-' + subgroupKey} style={{ padding: '2px 6px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={visible}
|
||||
onChange={() => this.toggleVisibility(visControllerRef, kinData, { type: 'subgroup', key: subgroupKey })}
|
||||
style={{ marginRight: '6px' }}
|
||||
/>
|
||||
<span title={subgroupKey}>{subgroupKey}</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// masters
|
||||
for (const [masterKey] of Object.entries(kinData.masterDict || {})) {
|
||||
const visible = this.isVisible(visControllerRef, { type: 'master', key: masterKey });
|
||||
kinBlock.push(
|
||||
<div key={'master-' + title + '-' + masterKey} style={{ padding: '2px 6px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={visible}
|
||||
onChange={() => this.toggleVisibility(visControllerRef, kinData, { type: 'master', key: masterKey })}
|
||||
style={{ marginRight: '6px' }}
|
||||
/>
|
||||
<span title={masterKey}>{masterKey}</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
blocks.push(<div key={'kin-block-' + title} className='msp-control-group-wrapper'>{kinBlock}</div>);
|
||||
return result;
|
||||
}
|
||||
|
||||
return <>{blocks}</>;
|
||||
}
|
||||
private async applyView(kinData: Kinemage, viewKey: string) {
|
||||
const snap = (kinData as any).viewSnapshots?.[viewKey];
|
||||
if (snap) {
|
||||
await applyViewSnapshot(this.plugin, snap as Partial<Camera.Snapshot>);
|
||||
}
|
||||
}
|
||||
|
||||
private async toggleVisibility(kinData: Kinemage, kinRef: string, target: { type: 'group' | 'subgroup' | 'master', key: string }) {
|
||||
try {
|
||||
if (target.type === 'group') {
|
||||
const g = kinData.groupDict[target.key];
|
||||
if (g) g.off = !g.off;
|
||||
} else if (target.type === 'subgroup') {
|
||||
const s = kinData.subgroupDict[target.key];
|
||||
if (s) s.off = !s.off;
|
||||
} else {
|
||||
const m = kinData.masterDict[target.key];
|
||||
if (m) m.visible = !m.visible;
|
||||
}
|
||||
|
||||
// Rebuild shapes for this kinemage using the state ref
|
||||
await rebuildShapesForKinemage(this.plugin, { ref: kinRef } as any);
|
||||
this.updateVisibility();
|
||||
} catch (e) {
|
||||
console.error('Failed to toggle kinemage visibility', e);
|
||||
}
|
||||
}
|
||||
|
||||
private async triggerAnimateForKin(kinData: Kinemage, kinRef: string, mode: 'animate' | '2animate') {
|
||||
try {
|
||||
if (mode === 'animate') {
|
||||
kinData.activeAnimateGroup = (kinData.activeAnimateGroup + 1) % Math.max(1, kinData.groupsAnimate.length);
|
||||
|
||||
// Make only the active animate group visible, hide the others (if any)
|
||||
for (let i = 0; i < kinData.groupsAnimate.length; i++) {
|
||||
const groupName = kinData.groupsAnimate[i];
|
||||
const groupInfo = kinData.groupDict[groupName];
|
||||
if (groupInfo) {
|
||||
groupInfo.off = (i !== kinData.activeAnimateGroup);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
kinData.activeAnimateGroup2 = (kinData.activeAnimateGroup2 + 1) % Math.max(1, kinData.groupsAnimate2.length);
|
||||
|
||||
// Make only the active animate group visible, hide the others (if any)
|
||||
for (let i = 0; i < kinData.groupsAnimate2.length; i++) {
|
||||
const groupName = kinData.groupsAnimate2[i];
|
||||
const groupInfo = kinData.groupDict[groupName];
|
||||
if (groupInfo) {
|
||||
groupInfo.off = (i !== kinData.activeAnimateGroup2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild shapes for this kinemage using the state ref
|
||||
await rebuildShapesForKinemage(this.plugin, { ref: kinRef } as any);
|
||||
this.updateVisibility();
|
||||
} catch (e) {
|
||||
console.error('Failed to trigger animate', e);
|
||||
}
|
||||
}
|
||||
|
||||
renderControls() {
|
||||
const kins = this.getKinemageList();
|
||||
if (kins.length === 0) return <div style={{ padding: '6px' }}>No Kinemage data</div>;
|
||||
|
||||
const blocks: React.ReactNode[] = [];
|
||||
for (const { kinData, ref } of kins) {
|
||||
const title = kinData.pdbfile || nameFromString(kinData.caption) || 'Kinemage';
|
||||
const kinBlock: React.ReactNode[] = [];
|
||||
|
||||
// Title
|
||||
kinBlock.push(
|
||||
<div key={'title-' + title} style={{ padding: '6px', fontWeight: 'bold', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
|
||||
// views
|
||||
const viewEntries = Object.entries(kinData.viewDict || {});
|
||||
if (viewEntries.length > 0) {
|
||||
for (const [viewKey, viewObj] of viewEntries) {
|
||||
const label = `View ${viewObj.name || `View ${viewKey}`}`;
|
||||
kinBlock.push(
|
||||
<div key={'view-' + title + '-' + viewKey} style={{ padding: '2px 6px' }}>
|
||||
<button
|
||||
className='msp-btn msp-btn-block'
|
||||
onClick={() => this.applyView(kinData, viewKey)}
|
||||
title={`Apply view: ${label}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// animate
|
||||
if (kinData.groupsAnimate && kinData.groupsAnimate.length > 0) {
|
||||
kinBlock.push(
|
||||
<div key={'anim-' + title} style={{ padding: '2px 6px' }}>
|
||||
<button
|
||||
className='msp-btn msp-btn-block'
|
||||
onClick={() => this.triggerAnimateForKin(kinData, ref, 'animate')}
|
||||
title='Cycle through animation frames'
|
||||
>
|
||||
Animate
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (kinData.groupsAnimate2 && kinData.groupsAnimate2.length > 0) {
|
||||
kinBlock.push(
|
||||
<div key={'anim2-' + title} style={{ padding: '2px 6px' }}>
|
||||
<button
|
||||
className='msp-btn msp-btn-block'
|
||||
onClick={() => this.triggerAnimateForKin(kinData, ref, '2animate')}
|
||||
title='Cycle through second animation frames'
|
||||
>
|
||||
Animate2
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// groups
|
||||
for (const [groupKey, groupInfo] of Object.entries(kinData.groupDict || {})) {
|
||||
if (!(groupInfo as any).nobutton) {
|
||||
const visible = !(groupInfo as any).off;
|
||||
// If this group is in animate or animate2, then add '*' before its groupKey name to indicate that it's an animation group
|
||||
const isAnimate = kinData.groupsAnimate?.includes(groupKey) || kinData.groupsAnimate2?.includes(groupKey);
|
||||
const label = isAnimate ? `* ${groupKey}` : groupKey;
|
||||
kinBlock.push(
|
||||
<div key={'group-' + title + '-' + groupKey} style={{ padding: '2px 6px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={visible}
|
||||
onChange={() => this.toggleVisibility(kinData, ref, { type: 'group', key: groupKey })}
|
||||
style={{ marginRight: '6px' }}
|
||||
/>
|
||||
<span title={label}>{label}</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// If this group is not dominant, find any subgroups of this group and show them here (indented) unless they have nobutton set
|
||||
if (!(groupInfo as any).dominant) {
|
||||
for (const [subgroupKey, subgroupInfo] of Object.entries(kinData.subgroupDict || {})) {
|
||||
if (subgroupKey.startsWith(groupKey + ':')) {
|
||||
if ((subgroupInfo as any).nobutton) continue;
|
||||
const visible = !(subgroupInfo as any).off;
|
||||
const subgroupLabel = subgroupKey.split(':')[1];
|
||||
kinBlock.push(
|
||||
<div key={'subgroup-' + title + '-' + subgroupKey} style={{ padding: '2px 6px', paddingLeft: '24px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={visible}
|
||||
onChange={() => this.toggleVisibility(kinData, ref, { type: 'subgroup', key: subgroupKey })}
|
||||
style={{ marginRight: '6px' }}
|
||||
/>
|
||||
<span title={subgroupLabel}>{subgroupLabel}</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// subgroups that don't belong to a group (standalone)
|
||||
for (const [subgroupKey, subgroupInfo] of Object.entries(kinData.subgroupDict || {})) {
|
||||
// if parent group present, those groups' subgroups are already shown when iterating groups
|
||||
if (subgroupKey.indexOf(':') !== -1) {
|
||||
// subgroups with parent group; skip here (shown under parent group)
|
||||
continue;
|
||||
}
|
||||
if ((subgroupInfo as any).nobutton) continue;
|
||||
const visible = !(subgroupInfo as any).off;
|
||||
kinBlock.push(
|
||||
<div key={'subgroup-' + title + '-' + subgroupKey} style={{ padding: '2px 6px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={visible}
|
||||
onChange={() => this.toggleVisibility(kinData, ref, { type: 'subgroup', key: subgroupKey })}
|
||||
style={{ marginRight: '6px' }}
|
||||
/>
|
||||
<span title={subgroupKey}>{subgroupKey}</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// masters
|
||||
for (const [masterKey, masterInfo] of Object.entries(kinData.masterDict || {})) {
|
||||
const visible = !!(masterInfo && (masterInfo as any).visible);
|
||||
kinBlock.push(
|
||||
<div key={'master-' + title + '-' + masterKey} style={{ padding: '2px 6px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={visible}
|
||||
onChange={() => this.toggleVisibility(kinData, ref, { type: 'master', key: masterKey })}
|
||||
style={{ marginRight: '6px' }}
|
||||
/>
|
||||
<span title={masterKey}>{masterKey}</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
blocks.push(<div key={'kin-block-' + title} className='msp-control-group-wrapper'>{kinBlock}</div>);
|
||||
}
|
||||
|
||||
return <>{blocks}</>;
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,7 @@ export const Canvas3DParams = {
|
||||
transparentBackground: PD.Boolean(false),
|
||||
checkeredTransparentBackground: PD.Boolean(false),
|
||||
dpoitIterations: PD.Numeric(2, { min: 1, max: 10, step: 1 }),
|
||||
enableAnimation: PD.Boolean(true, { description: 'Enable GPU time-based animations (wiggle/tumble).' }),
|
||||
pickPadding: PD.Numeric(3, { min: 0, max: 10, step: 1 }, { description: 'Extra pixels to around target to check in case target is empty.' }),
|
||||
userInteractionReleaseMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time before the user is not considered interacting anymore.' }),
|
||||
|
||||
@@ -479,6 +480,7 @@ namespace Canvas3D {
|
||||
const hiZ = new HiZPass(webgl, passes.draw, canvas, p.hiZ);
|
||||
|
||||
const renderer = Renderer.create(webgl, p.renderer);
|
||||
renderer.setProps({ enableAnimation: p.enableAnimation });
|
||||
renderer.setOcclusionTest(hiZ.isOccluded);
|
||||
|
||||
const shaderManager = new ShaderManager(webgl, scene);
|
||||
@@ -675,7 +677,7 @@ namespace Canvas3D {
|
||||
const xrChanged = xrManager.update(xrFrame);
|
||||
if (!xrChanged && xrFrame) return false;
|
||||
|
||||
const activeAnimation = renderer.props.enableAnimation && scene.hasAnimation;
|
||||
const activeAnimation = p.enableAnimation && scene.hasAnimation;
|
||||
const shouldRender = force || cameraChanged || resized || forceNextRender || xrChanged || activeAnimation;
|
||||
forceNextRender = false;
|
||||
|
||||
@@ -1069,6 +1071,7 @@ namespace Canvas3D {
|
||||
transparentBackground: p.transparentBackground,
|
||||
checkeredTransparentBackground: p.checkeredTransparentBackground,
|
||||
dpoitIterations: p.dpoitIterations,
|
||||
enableAnimation: p.enableAnimation,
|
||||
pickPadding: p.pickPadding,
|
||||
userInteractionReleaseMs: p.userInteractionReleaseMs,
|
||||
viewport: p.viewport,
|
||||
@@ -1314,6 +1317,10 @@ namespace Canvas3D {
|
||||
if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground;
|
||||
if (props.checkeredTransparentBackground !== undefined) p.checkeredTransparentBackground = props.checkeredTransparentBackground;
|
||||
if (props.dpoitIterations !== undefined) p.dpoitIterations = props.dpoitIterations;
|
||||
if (props.enableAnimation !== undefined) {
|
||||
p.enableAnimation = props.enableAnimation;
|
||||
renderer.setProps({ enableAnimation: p.enableAnimation });
|
||||
}
|
||||
if (props.pickPadding !== undefined) {
|
||||
p.pickPadding = props.pickPadding;
|
||||
pickHelper.setPickPadding(p.pickPadding);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
|
||||
@@ -484,45 +484,27 @@ export class SsaoPass {
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.downsample');
|
||||
}
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.half');
|
||||
if (multiScale) {
|
||||
// half-resolution viewport (matches dimensions of depthHalfTarget*)
|
||||
const hsx = Math.floor(sx * 0.5);
|
||||
const hsy = Math.floor(sy * 0.5);
|
||||
const hsw = Math.ceil(sw * 0.5);
|
||||
const hsh = Math.ceil(sh * 0.5);
|
||||
state.viewport(hsx, hsy, hsw, hsh);
|
||||
state.scissor(hsx, hsy, hsw, hsh);
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.half');
|
||||
this.depthHalfTargetOpaque.bind();
|
||||
this.depthHalfRenderableOpaque.render();
|
||||
if (includeTransparent) {
|
||||
this.depthHalfTargetTransparent.bind();
|
||||
this.depthHalfRenderableTransparent.render();
|
||||
}
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.half');
|
||||
}
|
||||
if (multiScale && includeTransparent) {
|
||||
this.depthHalfTargetTransparent.bind();
|
||||
this.depthHalfRenderableTransparent.render();
|
||||
}
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.half');
|
||||
|
||||
// quarter-resolution viewport (matches dimensions of depthQuarterTarget*)
|
||||
const qsx = Math.floor(sx * 0.25);
|
||||
const qsy = Math.floor(sy * 0.25);
|
||||
const qsw = Math.ceil(sw * 0.25);
|
||||
const qsh = Math.ceil(sh * 0.25);
|
||||
state.viewport(qsx, qsy, qsw, qsh);
|
||||
state.scissor(qsx, qsy, qsw, qsh);
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.quarter');
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.quarter');
|
||||
if (multiScale) {
|
||||
this.depthQuarterTargetOpaque.bind();
|
||||
this.depthQuarterRenderableOpaque.render();
|
||||
if (includeTransparent) {
|
||||
this.depthQuarterTargetTransparent.bind();
|
||||
this.depthQuarterRenderableTransparent.render();
|
||||
}
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.quarter');
|
||||
|
||||
// restore full-scale viewport for SSAO + blur passes
|
||||
state.viewport(sx, sy, sw, sh);
|
||||
state.scissor(sx, sy, sw, sh);
|
||||
}
|
||||
if (multiScale && includeTransparent) {
|
||||
this.depthQuarterTargetTransparent.bind();
|
||||
this.depthQuarterRenderableTransparent.render();
|
||||
}
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.quarter');
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.opaque');
|
||||
this.ssaoDepthTexture.attachFramebuffer(this.framebuffer, 'color0');
|
||||
|
||||
@@ -78,7 +78,7 @@ export const apply_light_color = `
|
||||
}
|
||||
#pragma unroll_loop_end
|
||||
|
||||
outgoingLight += physicalMaterial.diffuseColor * uAmbientColor;
|
||||
outgoingLight += physicalMaterial.diffuseColor * luminance(uAmbientColor);
|
||||
#else
|
||||
ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));
|
||||
|
||||
|
||||
@@ -133,7 +133,6 @@ export enum InteractionType {
|
||||
Hydrophobic = 6,
|
||||
MetalCoordination = 7,
|
||||
WeakHydrogenBond = 8,
|
||||
WaterBridge = 9,
|
||||
}
|
||||
|
||||
export function interactionTypeLabel(type: InteractionType): string {
|
||||
@@ -154,8 +153,6 @@ export function interactionTypeLabel(type: InteractionType): string {
|
||||
return 'Pi Stacking';
|
||||
case InteractionType.WeakHydrogenBond:
|
||||
return 'Weak Hydrogen Bond';
|
||||
case InteractionType.WaterBridge:
|
||||
return 'Water Bridge';
|
||||
case InteractionType.Unknown:
|
||||
return 'Unknown Interaction';
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { FeatureType, FeatureGroup, InteractionType } from './common';
|
||||
import { ContactProvider } from './contacts';
|
||||
import { MoleculeType, ProteinBackboneAtoms } from '../../../mol-model/structure/model/types';
|
||||
|
||||
export const GeometryParams = {
|
||||
const GeometryParams = {
|
||||
distanceMax: PD.Numeric(3.5, { min: 1, max: 5, step: 0.1 }),
|
||||
backbone: PD.Boolean(true, { description: 'Include backbone-to-backbone hydrogen bonds' }),
|
||||
accAngleDevMax: PD.Numeric(45, { min: 0, max: 180, step: 1 }, { description: 'Max deviation from ideal acceptor angle' }),
|
||||
@@ -29,7 +29,7 @@ export const GeometryParams = {
|
||||
accOutOfPlaneAngleMax: PD.Numeric(90, { min: 0, max: 180, step: 1 }),
|
||||
donOutOfPlaneAngleMax: PD.Numeric(45, { min: 0, max: 180, step: 1 }),
|
||||
};
|
||||
export type GeometryParams = typeof GeometryParams
|
||||
type GeometryParams = typeof GeometryParams
|
||||
type GeometryProps = PD.Values<GeometryParams>
|
||||
|
||||
const HydrogenBondsParams = {
|
||||
@@ -208,7 +208,7 @@ function isWeakHydrogenBond(ti: FeatureType, tj: FeatureType) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getGeometryOptions(props: GeometryProps) {
|
||||
function getGeometryOptions(props: GeometryProps) {
|
||||
return {
|
||||
ignoreHydrogens: props.ignoreHydrogens,
|
||||
includeBackbone: props.backbone,
|
||||
@@ -218,7 +218,7 @@ export function getGeometryOptions(props: GeometryProps) {
|
||||
maxDonOutOfPlaneAngle: degToRad(props.donOutOfPlaneAngleMax),
|
||||
};
|
||||
}
|
||||
export type GeometryOptions = ReturnType<typeof getGeometryOptions>
|
||||
type GeometryOptions = ReturnType<typeof getGeometryOptions>
|
||||
|
||||
function getHydrogenBondsOptions(props: HydrogenBondsProps) {
|
||||
return {
|
||||
@@ -232,7 +232,7 @@ type HydrogenBondsOptions = ReturnType<typeof getHydrogenBondsOptions>
|
||||
|
||||
const deg120InRad = degToRad(120);
|
||||
|
||||
export function checkGeometry(structure: Structure, don: Features.Info, acc: Features.Info, opts: GeometryOptions): true | undefined {
|
||||
function checkGeometry(structure: Structure, don: Features.Info, acc: Features.Info, opts: GeometryOptions): true | undefined {
|
||||
const donIndex = don.members[don.offsets[don.feature]];
|
||||
const accIndex = acc.members[acc.offsets[acc.feature]];
|
||||
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { Structure, Unit, Bond, StructureElement } from '../../../mol-model/structure';
|
||||
import { Structure, Unit, Bond } from '../../../mol-model/structure';
|
||||
import { Features, FeaturesBuilder } from './features';
|
||||
import { ValenceModelProvider } from '../valence-model';
|
||||
import { InteractionsIntraContacts, InteractionsInterContacts, FeatureType, InteractionType, InteractionFlag, interactionTypeLabel } from './common';
|
||||
import { InteractionsIntraContacts, InteractionsInterContacts, FeatureType, interactionTypeLabel } from './common';
|
||||
import { IntraContactsBuilder, InterContactsBuilder } from './contacts-builder';
|
||||
import { IntMap, OrderedSet } from '../../../mol-data/int';
|
||||
import { IntMap } from '../../../mol-data/int';
|
||||
import { addUnitContacts, ContactTester, addStructureContacts, ContactsParams, ContactsProps } from './contacts';
|
||||
import { HalogenDonorProvider, HalogenAcceptorProvider, HalogenBondsProvider } from './halogen-bonds';
|
||||
import { HydrogenDonorProvider, WeakHydrogenDonorProvider, HydrogenAcceptorProvider, HydrogenBondsProvider, WeakHydrogenBondsProvider } from './hydrogen-bonds';
|
||||
import { WaterBridgesProvider } from './water-bridges';
|
||||
import { NegativChargeProvider, PositiveChargeProvider, AromaticRingProvider, IonicProvider, PiStackingProvider, CationPiProvider } from './charged';
|
||||
import { HydrophobicAtomProvider, HydrophobicProvider } from './hydrophobic';
|
||||
import { SetUtils } from '../../../mol-util/set';
|
||||
@@ -26,26 +25,10 @@ import { DataLocation } from '../../../mol-model/location';
|
||||
import { CentroidHelper } from '../../../mol-math/geometry/centroid-helper';
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { DataLoci } from '../../../mol-model/loci';
|
||||
import { bondLabel, bundleLabel, LabelGranularity } from '../../../mol-theme/label';
|
||||
import { bondLabel, LabelGranularity } from '../../../mol-theme/label';
|
||||
import { ObjectKeys } from '../../../mol-util/type-helpers';
|
||||
|
||||
export { Interactions, Bridges };
|
||||
export type { BridgeContact, BridgeContacts };
|
||||
|
||||
interface BridgeContact {
|
||||
readonly unitA: number
|
||||
readonly indexA: Features.FeatureIndex
|
||||
readonly unitB: number
|
||||
readonly indexB: Features.FeatureIndex
|
||||
/** mediator unit id */
|
||||
readonly unitM: number
|
||||
/** mediator feature facing endpoint A */
|
||||
readonly indexMA: Features.FeatureIndex
|
||||
/** mediator feature facing endpoint B */
|
||||
readonly indexMB: Features.FeatureIndex
|
||||
props: { type: InteractionType, flag: InteractionFlag }
|
||||
}
|
||||
type BridgeContacts = ReadonlyArray<BridgeContact>
|
||||
export { Interactions };
|
||||
|
||||
interface Interactions {
|
||||
/** Features of each unit */
|
||||
@@ -54,8 +37,6 @@ interface Interactions {
|
||||
unitsContacts: IntMap<InteractionsIntraContacts>
|
||||
/** Interactions between units */
|
||||
contacts: InteractionsInterContacts
|
||||
/** Bridge-mediated interactions covering the whole structure */
|
||||
bridges: BridgeContacts
|
||||
}
|
||||
|
||||
namespace Interactions {
|
||||
@@ -148,93 +129,6 @@ namespace Interactions {
|
||||
}
|
||||
}
|
||||
|
||||
namespace Bridges {
|
||||
export interface Data {
|
||||
readonly structure: Structure
|
||||
readonly bridges: BridgeContacts
|
||||
readonly unitsFeatures: IntMap<Features>
|
||||
}
|
||||
|
||||
export interface Element { bridgeIndex: number }
|
||||
|
||||
export interface Location extends DataLocation<Data, Element> {}
|
||||
|
||||
export function Location(data: Data, bridgeIndex = 0): Location {
|
||||
return DataLocation('bridges', data, { bridgeIndex });
|
||||
}
|
||||
|
||||
export function isLocation(x: any): x is Location {
|
||||
return !!x && x.kind === 'data-location' && x.tag === 'bridges';
|
||||
}
|
||||
|
||||
export interface Loci extends DataLoci<Data, Element> {}
|
||||
|
||||
export function Loci(data: Data, elements: ReadonlyArray<Element>): Loci {
|
||||
return DataLoci('bridges', data, elements,
|
||||
bs => getBoundingSphere(data, elements, bs),
|
||||
() => getLabel(data, elements));
|
||||
}
|
||||
|
||||
export function isLoci(x: any): x is Loci {
|
||||
return !!x && x.kind === 'data-loci' && x.tag === 'bridges';
|
||||
}
|
||||
|
||||
function getLabel(data: Data, elements: ReadonlyArray<Element>): string {
|
||||
const e = elements[0];
|
||||
if (e === undefined) return '';
|
||||
|
||||
const { structure, bridges, unitsFeatures } = data;
|
||||
const bridge = bridges[e.bridgeIndex];
|
||||
|
||||
const uA = structure.unitMap.get(bridge.unitA) as Unit.Atomic;
|
||||
const fA = unitsFeatures.get(bridge.unitA);
|
||||
const uM = structure.unitMap.get(bridge.unitM) as Unit.Atomic;
|
||||
const fM = unitsFeatures.get(bridge.unitM);
|
||||
const uB = structure.unitMap.get(bridge.unitB) as Unit.Atomic;
|
||||
const fB = unitsFeatures.get(bridge.unitB);
|
||||
|
||||
const options = { granularity: 'element' as LabelGranularity };
|
||||
if (fA.offsets[bridge.indexA + 1] - fA.offsets[bridge.indexA] > 1 ||
|
||||
fB.offsets[bridge.indexB + 1] - fB.offsets[bridge.indexB] > 1) {
|
||||
options.granularity = 'residue';
|
||||
}
|
||||
|
||||
return [
|
||||
interactionTypeLabel(bridge.props.type),
|
||||
bundleLabel({ loci: [
|
||||
StructureElement.Loci(structure, [{ unit: uA, indices: OrderedSet.ofSingleton(fA.members[fA.offsets[bridge.indexA]] as StructureElement.UnitIndex) }]),
|
||||
StructureElement.Loci(structure, [{ unit: uM, indices: OrderedSet.ofSingleton(fM.members[fM.offsets[bridge.indexMA]] as StructureElement.UnitIndex) }]),
|
||||
StructureElement.Loci(structure, [{ unit: uB, indices: OrderedSet.ofSingleton(fB.members[fB.offsets[bridge.indexB]] as StructureElement.UnitIndex) }]),
|
||||
] }, options),
|
||||
].join('</br>');
|
||||
}
|
||||
|
||||
function getBoundingSphere(data: Data, elements: ReadonlyArray<Element>, boundingSphere: Sphere3D) {
|
||||
return CentroidHelper.fromPairProvider(elements.length * 2, (i, pA, pB) => {
|
||||
const bridge = data.bridges[elements[i >> 1].bridgeIndex];
|
||||
|
||||
const uA = data.structure.unitMap.get(bridge.unitA) as Unit.Atomic;
|
||||
const fA = data.unitsFeatures.get(bridge.unitA);
|
||||
const uM = data.structure.unitMap.get(bridge.unitM) as Unit.Atomic;
|
||||
const fM = data.unitsFeatures.get(bridge.unitM);
|
||||
const uB = data.structure.unitMap.get(bridge.unitB) as Unit.Atomic;
|
||||
const fB = data.unitsFeatures.get(bridge.unitB);
|
||||
|
||||
const aIdx = fA.members[fA.offsets[bridge.indexA]];
|
||||
const mIdx = fM.members[fM.offsets[bridge.indexMA]];
|
||||
const bIdx = fB.members[fB.offsets[bridge.indexB]];
|
||||
|
||||
if ((i & 1) === 0) {
|
||||
uA.conformation.position(uA.elements[aIdx], pA);
|
||||
uM.conformation.position(uM.elements[mIdx], pB);
|
||||
} else {
|
||||
uM.conformation.position(uM.elements[mIdx], pA);
|
||||
uB.conformation.position(uB.elements[bIdx], pB);
|
||||
}
|
||||
}, boundingSphere);
|
||||
}
|
||||
}
|
||||
|
||||
const FeatureProviders = [
|
||||
HydrogenDonorProvider, WeakHydrogenDonorProvider, HydrogenAcceptorProvider,
|
||||
NegativChargeProvider, PositiveChargeProvider, AromaticRingProvider,
|
||||
@@ -280,30 +174,8 @@ export const ContactProviderParams = getProvidersParams([
|
||||
// 'weak-hydrogen-bonds',
|
||||
]);
|
||||
|
||||
const BridgeProviders = {
|
||||
'water-bridges': WaterBridgesProvider,
|
||||
};
|
||||
type BridgeProviders = typeof BridgeProviders
|
||||
|
||||
function getBridgeProviderParams(defaultOn: string[] = []) {
|
||||
const params: { [k in keyof BridgeProviders]: PD.Mapped<PD.NamedParamUnion<{
|
||||
on: PD.Group<BridgeProviders[k]['params']>
|
||||
off: PD.Group<{}>
|
||||
}>> } = Object.create(null);
|
||||
|
||||
Object.keys(BridgeProviders).forEach(k => {
|
||||
(params as any)[k] = PD.MappedStatic(defaultOn.includes(k) ? 'on' : 'off', {
|
||||
on: PD.Group(BridgeProviders[k as keyof BridgeProviders].params),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true });
|
||||
});
|
||||
return params;
|
||||
}
|
||||
export const BridgeProviderParams = getBridgeProviderParams([]);
|
||||
|
||||
export const InteractionsParams = {
|
||||
providers: PD.Group(ContactProviderParams, { isFlat: true }),
|
||||
bridges: PD.Group(BridgeProviderParams, { isFlat: true }),
|
||||
contacts: PD.Group(ContactsParams, { label: 'Advanced Options' }),
|
||||
};
|
||||
export type InteractionsParams = typeof InteractionsParams
|
||||
@@ -330,9 +202,6 @@ export async function computeInteractions(ctx: CustomProperty.Context, structure
|
||||
|
||||
const requiredFeatures = new Set<FeatureType>();
|
||||
contactTesters.forEach(l => SetUtils.add(requiredFeatures, l.requiredFeatures));
|
||||
ObjectKeys(BridgeProviders).forEach(k => {
|
||||
if (p.bridges[k].name === 'on') SetUtils.add(requiredFeatures, BridgeProviders[k].requiredFeatures);
|
||||
});
|
||||
const featureProviders = FeatureProviders.filter(f => SetUtils.areIntersecting(requiredFeatures, f.types));
|
||||
|
||||
const unitsFeatures = IntMap.Mutable<Features>();
|
||||
@@ -359,9 +228,8 @@ export async function computeInteractions(ctx: CustomProperty.Context, structure
|
||||
}
|
||||
|
||||
const contacts = findInterUnitContacts(structure, unitsFeatures, contactTesters, p.contacts, options);
|
||||
const bridges = findBridges(structure, unitsFeatures, p.bridges);
|
||||
const interactions = { unitsFeatures, unitsContacts, contacts, bridges };
|
||||
|
||||
const interactions = { unitsFeatures, unitsContacts, contacts };
|
||||
refineInteractions(structure, interactions);
|
||||
return interactions;
|
||||
}
|
||||
@@ -392,19 +260,6 @@ function findIntraUnitContacts(structure: Structure, unit: Unit, features: Featu
|
||||
return builder.getContacts();
|
||||
}
|
||||
|
||||
function findBridges(structure: Structure, unitsFeatures: IntMap<Features>, props: PD.Values<typeof BridgeProviderParams>): BridgeContacts {
|
||||
const bridges: BridgeContact[] = [];
|
||||
|
||||
ObjectKeys(BridgeProviders).forEach(k => {
|
||||
const { name, params } = props[k];
|
||||
if (name === 'on') {
|
||||
for (const b of BridgeProviders[k].find(structure, unitsFeatures, params as any)) bridges.push(b);
|
||||
}
|
||||
});
|
||||
|
||||
return bridges;
|
||||
}
|
||||
|
||||
function findInterUnitContacts(structure: Structure, unitsFeatures: IntMap<Features>, contactTesters: ReadonlyArray<ContactTester>, props: ContactsProps, options?: ComputeInterctionsOptions) {
|
||||
const builder = InterContactsBuilder.create();
|
||||
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Sebastian Bittrich <sebastian.m.bittrich@gmail.com>
|
||||
*
|
||||
* based in part on NGL (https://github.com/arose/ngl)
|
||||
*/
|
||||
|
||||
import { Interactions } from './interactions';
|
||||
import { InteractionType, InteractionFlag, InteractionsIntraContacts, FeatureType, InteractionsInterContacts } from './common';
|
||||
import { Unit, Structure, StructureElement } from '../../../mol-model/structure';
|
||||
import { Unit, Structure } from '../../../mol-model/structure';
|
||||
import { Features } from './features';
|
||||
import { cantorPairing } from '../../../mol-data/util/hash-functions';
|
||||
|
||||
interface ContactRefiner {
|
||||
isApplicable: (type: InteractionType) => boolean
|
||||
@@ -29,7 +27,6 @@ export function refineInteractions(structure: Structure, interactions: Interacti
|
||||
saltBridgeRefiner(structure, interactions),
|
||||
piStackingRefiner(structure, interactions),
|
||||
metalCoordinationRefiner(structure, interactions),
|
||||
waterBridgeRefiner(structure, interactions),
|
||||
];
|
||||
|
||||
for (let i = 0, il = contacts.edgeCount; i < il; ++i) {
|
||||
@@ -281,117 +278,4 @@ function metalCoordinationRefiner(structure: Structure, interactions: Interactio
|
||||
filterIntra([InteractionType.MetalCoordination], index, infoA, infoB, interactions.unitsContacts.get(infoA.unit.id));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function waterBridgeRefiner(_structure: Structure, interactions: Interactions): ContactRefiner {
|
||||
const { contacts, bridges, unitsFeatures } = interactions;
|
||||
|
||||
type AtomKey = number;
|
||||
type AtomPairSet = Map<AtomKey, Set<AtomKey>>;
|
||||
|
||||
function atomKey(unitId: number, atomIndex: StructureElement.UnitIndex): AtomKey {
|
||||
return cantorPairing(unitId, atomIndex);
|
||||
}
|
||||
|
||||
function featureMember(features: Features, featureIndex: Features.FeatureIndex): StructureElement.UnitIndex {
|
||||
return features.members[features.offsets[featureIndex]] as StructureElement.UnitIndex;
|
||||
}
|
||||
|
||||
function addAtomPair(
|
||||
set: AtomPairSet,
|
||||
unitA: number,
|
||||
atomA: StructureElement.UnitIndex,
|
||||
unitB: number,
|
||||
atomB: StructureElement.UnitIndex
|
||||
) {
|
||||
const a = atomKey(unitA, atomA);
|
||||
const b = atomKey(unitB, atomB);
|
||||
|
||||
let bs = set.get(a);
|
||||
if (bs === undefined) {
|
||||
bs = new Set();
|
||||
set.set(a, bs);
|
||||
}
|
||||
bs.add(b);
|
||||
|
||||
let as = set.get(b);
|
||||
if (as === undefined) {
|
||||
as = new Set();
|
||||
set.set(b, as);
|
||||
}
|
||||
as.add(a);
|
||||
}
|
||||
|
||||
function hasAtomPair(
|
||||
set: AtomPairSet,
|
||||
unitA: number,
|
||||
atomA: StructureElement.UnitIndex,
|
||||
unitB: number,
|
||||
atomB: StructureElement.UnitIndex
|
||||
): boolean {
|
||||
return set.get(atomKey(unitA, atomA))?.has(atomKey(unitB, atomB)) === true;
|
||||
}
|
||||
|
||||
function hasInfoPair(set: AtomPairSet, infoA: Features.Info, infoB: Features.Info): boolean {
|
||||
const { offsets: offsetsA, members: membersA, feature: featureA } = infoA;
|
||||
const { offsets: offsetsB, members: membersB, feature: featureB } = infoB;
|
||||
|
||||
for (let i = offsetsA[featureA], il = offsetsA[featureA + 1]; i < il; ++i) {
|
||||
const a = membersA[i] as StructureElement.UnitIndex;
|
||||
|
||||
for (let j = offsetsB[featureB], jl = offsetsB[featureB + 1]; j < jl; ++j) {
|
||||
const b = membersB[j] as StructureElement.UnitIndex;
|
||||
|
||||
if (hasAtomPair(set, infoA.unit.id, a, infoB.unit.id, b)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const bridgeLegs: AtomPairSet = new Map();
|
||||
|
||||
for (const wb of bridges) {
|
||||
if (wb.props.type !== InteractionType.WaterBridge) continue;
|
||||
|
||||
const fA = unitsFeatures.get(wb.unitA);
|
||||
const fM = unitsFeatures.get(wb.unitM);
|
||||
const fB = unitsFeatures.get(wb.unitB);
|
||||
|
||||
if (!fA || !fM || !fB) continue;
|
||||
|
||||
const atomA = featureMember(fA, wb.indexA);
|
||||
const atomMA = featureMember(fM, wb.indexMA);
|
||||
const atomMB = featureMember(fM, wb.indexMB);
|
||||
const atomB = featureMember(fB, wb.indexB);
|
||||
|
||||
// donor atom ↔ water oxygen
|
||||
addAtomPair(bridgeLegs, wb.unitA, atomA, wb.unitM, atomMA);
|
||||
|
||||
// water oxygen ↔ acceptor atom
|
||||
addAtomPair(bridgeLegs, wb.unitM, atomMB, wb.unitB, atomB);
|
||||
}
|
||||
|
||||
let intraContacts: InteractionsIntraContacts | undefined;
|
||||
|
||||
return {
|
||||
isApplicable: (type: InteractionType) => {
|
||||
return bridgeLegs.size > 0 && type === InteractionType.HydrogenBond;
|
||||
},
|
||||
handleInterContact: (index: number, infoA: Features.Info, infoB: Features.Info) => {
|
||||
if (hasInfoPair(bridgeLegs, infoA, infoB)) {
|
||||
contacts.edges[index].props.flag = InteractionFlag.Filtered;
|
||||
}
|
||||
},
|
||||
startUnit: (_unit: Unit.Atomic, contacts: InteractionsIntraContacts) => {
|
||||
intraContacts = contacts;
|
||||
},
|
||||
handleIntraContact: (index: number, infoA: Features.Info, infoB: Features.Info) => {
|
||||
if (!intraContacts) return;
|
||||
|
||||
if (hasInfoPair(bridgeLegs, infoA, infoB)) {
|
||||
intraContacts.edgeProps.flag[index] = InteractionFlag.Filtered;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,331 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sebastian Bittrich <sebastian.m.bittrich@gmail.com>
|
||||
*/
|
||||
|
||||
import { Structure, Unit, StructureElement } from '../../../mol-model/structure';
|
||||
import { IntMap } from '../../../mol-data/int';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { MoleculeType, NucleicBackboneAtoms, ProteinBackboneAtoms } from '../../../mol-model/structure/model/types';
|
||||
import { StructureLookup3DResultContext } from '../../../mol-model/structure/structure/util/lookup3d';
|
||||
import { Features } from './features';
|
||||
import { FeatureType, InteractionType, InteractionFlag } from './common';
|
||||
import { GeometryOptions, checkGeometry } from './hydrogen-bonds';
|
||||
import { degToRad } from '../../../mol-math/misc';
|
||||
import { cantorPairing } from '../../../mol-data/util/hash-functions';
|
||||
|
||||
export type { WaterBridgeContact, WaterBridgeContacts };
|
||||
|
||||
interface WaterBridgeContact {
|
||||
/** non-water donor unit id */
|
||||
readonly unitA: number
|
||||
/** donor feature index in unitA */
|
||||
readonly indexA: Features.FeatureIndex
|
||||
/** non-water acceptor unit id */
|
||||
readonly unitB: number
|
||||
/** acceptor feature index in unitB */
|
||||
readonly indexB: Features.FeatureIndex
|
||||
/** bridging water unit id */
|
||||
readonly unitM: number
|
||||
/** water oxygen as HydrogenAcceptor (leg: donor → water) */
|
||||
readonly indexMA: Features.FeatureIndex
|
||||
/** water oxygen as HydrogenDonor (leg: water → acceptor) */
|
||||
readonly indexMB: Features.FeatureIndex
|
||||
props: { type: InteractionType.WaterBridge, flag: InteractionFlag }
|
||||
}
|
||||
|
||||
type WaterBridgeContacts = ReadonlyArray<WaterBridgeContact>;
|
||||
|
||||
export const WaterBridgesParams = {
|
||||
backbone: PD.Boolean(true, { description: 'Include backbone hydrogen bonds' }),
|
||||
ignoreHydrogens: PD.Boolean(true, { description: 'Ignore explicit hydrogens in geometric constraints' }),
|
||||
legDistMin: PD.Numeric(2.5, { min: 1, max: 4, step: 0.1 }, { description: 'Minimum leg distance (Å)' }),
|
||||
legDistMax: PD.Numeric(4.1, { min: 1, max: 6, step: 0.1 }, { description: 'Maximum leg distance (Å)' }),
|
||||
donAngleDevMax: PD.Numeric(80, { min: 0, max: 180, step: 1 }, { description: 'Max deviation from ideal donor angle' }),
|
||||
accAngleDevMax: PD.Numeric(50, { min: 0, max: 180, step: 1 }, { description: 'Max deviation from ideal acceptor angle' }),
|
||||
donOutOfPlaneAngleMax: PD.Numeric(45, { min: 0, max: 180, step: 1 }),
|
||||
accOutOfPlaneAngleMax: PD.Numeric(90, { min: 0, max: 180, step: 1 }),
|
||||
omegaMin: PD.Numeric(71, { min: 0, max: 180, step: 1 }, { description: 'Minimum A–W–B angle (°)' }),
|
||||
omegaMax: PD.Numeric(140, { min: 0, max: 180, step: 1 }, { description: 'Maximum A–W–B angle (°)' }),
|
||||
};
|
||||
export type WaterBridgesParams = typeof WaterBridgesParams;
|
||||
export type WaterBridgesProps = PD.Values<WaterBridgesParams>;
|
||||
|
||||
export const WaterBridgesProvider = {
|
||||
requiredFeatures: new Set([FeatureType.HydrogenDonor, FeatureType.HydrogenAcceptor]),
|
||||
params: WaterBridgesParams,
|
||||
find: findWaterBridgeContacts,
|
||||
};
|
||||
|
||||
function isWater(unit: Unit.Atomic, index: StructureElement.UnitIndex): boolean {
|
||||
return unit.model.atomicHierarchy.derived.residue.moleculeType[
|
||||
unit.residueIndex[unit.elements[index]]
|
||||
] === MoleculeType.Water;
|
||||
}
|
||||
|
||||
function isBackboneAtom(unit: Unit.Atomic, index: StructureElement.UnitIndex): boolean {
|
||||
const element = unit.elements[index];
|
||||
const moleculeType = unit.model.atomicHierarchy.derived.residue.moleculeType[unit.residueIndex[element]];
|
||||
if (moleculeType !== MoleculeType.Protein && moleculeType !== MoleculeType.RNA && moleculeType !== MoleculeType.DNA) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const atomId = unit.model.atomicHierarchy.atoms.label_atom_id.value(element);
|
||||
if (moleculeType === MoleculeType.Protein) {
|
||||
return ProteinBackboneAtoms.has(atomId);
|
||||
}
|
||||
|
||||
return NucleicBackboneAtoms.has(atomId);
|
||||
}
|
||||
|
||||
const _lookupCtx = StructureLookup3DResultContext();
|
||||
|
||||
type Candidate = {
|
||||
unit: Unit.Atomic
|
||||
featureIdx: Features.FeatureIndex
|
||||
memberIdx: StructureElement.UnitIndex
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
distSq: number
|
||||
};
|
||||
|
||||
type FeatureKey = number;
|
||||
|
||||
function featureKey(unitId: number, featureIndex: Features.FeatureIndex): FeatureKey {
|
||||
return cantorPairing(unitId, featureIndex);
|
||||
}
|
||||
|
||||
type BestBridge = { contact: WaterBridgeContact; combinedDistSq: number };
|
||||
type BestBridgeMap = Map<FeatureKey, Map<FeatureKey, BestBridge>>;
|
||||
|
||||
function getBestBridge(best: BestBridgeMap, donorKey: FeatureKey, acceptorKey: FeatureKey): BestBridge | undefined {
|
||||
return best.get(donorKey)?.get(acceptorKey);
|
||||
}
|
||||
|
||||
function setBestBridge(best: BestBridgeMap, donorKey: FeatureKey, acceptorKey: FeatureKey, value: BestBridge) {
|
||||
let acceptors = best.get(donorKey);
|
||||
if (acceptors === undefined) {
|
||||
acceptors = new Map();
|
||||
best.set(donorKey, acceptors);
|
||||
}
|
||||
acceptors.set(acceptorKey, value);
|
||||
}
|
||||
|
||||
function bestBridgeValues(best: BestBridgeMap): BestBridge[] {
|
||||
const values: BestBridge[] = [];
|
||||
for (const acceptors of best.values()) {
|
||||
for (const value of acceptors.values()) values.push(value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function checkOmega(don: Candidate, posW: Vec3, acc: Candidate, cosOmegaMin: number, cosOmegaMax: number): boolean {
|
||||
const ax = don.x - posW[0];
|
||||
const ay = don.y - posW[1];
|
||||
const az = don.z - posW[2];
|
||||
|
||||
const bx = acc.x - posW[0];
|
||||
const by = acc.y - posW[1];
|
||||
const bz = acc.z - posW[2];
|
||||
|
||||
const aLenSq = ax * ax + ay * ay + az * az;
|
||||
const bLenSq = bx * bx + by * by + bz * bz;
|
||||
|
||||
if (aLenSq === 0 || bLenSq === 0) return false;
|
||||
|
||||
const cosOmega = (ax * bx + ay * by + az * bz) / Math.sqrt(aLenSq * bLenSq);
|
||||
|
||||
// cos decreases monotonically on [0, pi], so:
|
||||
// omega >= omegaMin && omega <= omegaMax
|
||||
// is equivalent to:
|
||||
// cos(omega) <= cos(omegaMin) && cos(omega) >= cos(omegaMax)
|
||||
return cosOmega <= cosOmegaMin && cosOmega >= cosOmegaMax;
|
||||
}
|
||||
|
||||
export function findWaterBridgeContacts(
|
||||
structure: Structure,
|
||||
unitsFeatures: IntMap<Features>,
|
||||
props: WaterBridgesProps
|
||||
): WaterBridgeContacts {
|
||||
const legOpts: GeometryOptions = {
|
||||
ignoreHydrogens: props.ignoreHydrogens,
|
||||
includeBackbone: props.backbone,
|
||||
maxAccAngleDev: degToRad(props.accAngleDevMax),
|
||||
maxDonAngleDev: degToRad(props.donAngleDevMax),
|
||||
maxAccOutOfPlaneAngle: degToRad(props.accOutOfPlaneAngleMax),
|
||||
maxDonOutOfPlaneAngle: degToRad(props.donOutOfPlaneAngleMax),
|
||||
};
|
||||
|
||||
const legDistMinSq = props.legDistMin * props.legDistMin;
|
||||
const legDistMaxSq = props.legDistMax * props.legDistMax;
|
||||
|
||||
const omegaMinRad = degToRad(props.omegaMin);
|
||||
const omegaMaxRad = degToRad(props.omegaMax);
|
||||
|
||||
if (omegaMinRad > omegaMaxRad) return [];
|
||||
|
||||
const cosOmegaMin = Math.cos(omegaMinRad);
|
||||
const cosOmegaMax = Math.cos(omegaMaxRad);
|
||||
|
||||
// Best bridge per unique donor/acceptor feature pair across all water molecules.
|
||||
const best: BestBridgeMap = new Map();
|
||||
|
||||
const wPos = Vec3();
|
||||
const candidatePos = Vec3();
|
||||
|
||||
for (const unitW of structure.units) {
|
||||
if (!Unit.isAtomic(unitW)) continue;
|
||||
|
||||
const featW = unitsFeatures.get(unitW.id);
|
||||
if (!featW || featW.count === 0) continue;
|
||||
|
||||
// Map each water-oxygen local index to its acceptor and donor feature indices.
|
||||
const waterMap = new Map<StructureElement.UnitIndex, {
|
||||
acc: Features.FeatureIndex | undefined,
|
||||
don: Features.FeatureIndex | undefined
|
||||
}>();
|
||||
|
||||
for (let fi = 0 as Features.FeatureIndex; fi < featW.count; fi++) {
|
||||
const mi = featW.members[featW.offsets[fi]] as StructureElement.UnitIndex;
|
||||
if (!isWater(unitW, mi)) continue;
|
||||
|
||||
const t = featW.types[fi];
|
||||
if (t !== FeatureType.HydrogenAcceptor && t !== FeatureType.HydrogenDonor) continue;
|
||||
|
||||
let e = waterMap.get(mi);
|
||||
if (!e) waterMap.set(mi, (e = { acc: undefined, don: undefined }));
|
||||
|
||||
if (t === FeatureType.HydrogenAcceptor) e.acc = fi;
|
||||
else e.don = fi;
|
||||
}
|
||||
|
||||
if (waterMap.size === 0) continue;
|
||||
|
||||
const infoWAcc = Features.Info(structure, unitW, featW);
|
||||
const infoWDon = Features.Info(structure, unitW, featW);
|
||||
|
||||
for (const [waterAtomIdx, { acc: accFW, don: donFW }] of waterMap) {
|
||||
if (accFW === undefined || donFW === undefined) continue;
|
||||
|
||||
unitW.conformation.position(unitW.elements[waterAtomIdx], wPos);
|
||||
|
||||
infoWAcc.feature = accFW;
|
||||
infoWDon.feature = donFW;
|
||||
|
||||
const { count, indices, units: hitUnits } =
|
||||
structure.lookup3d.find(wPos[0], wPos[1], wPos[2], props.legDistMax, _lookupCtx);
|
||||
|
||||
const donors: Candidate[] = [];
|
||||
const acceptors: Candidate[] = [];
|
||||
|
||||
const donorKeys = new Set<FeatureKey>();
|
||||
const acceptorKeys = new Set<FeatureKey>();
|
||||
|
||||
for (let r = 0; r < count; r++) {
|
||||
const hitUnit = hitUnits[r];
|
||||
if (!Unit.isAtomic(hitUnit)) continue;
|
||||
|
||||
const atomicUnit = hitUnit as Unit.Atomic;
|
||||
const hitLocalIdx = indices[r] as StructureElement.UnitIndex;
|
||||
|
||||
// Only skip the water atom itself. Other atoms in the same unit can still be valid.
|
||||
if (atomicUnit === unitW && hitLocalIdx === waterAtomIdx) continue;
|
||||
if (isWater(atomicUnit, hitLocalIdx)) continue;
|
||||
|
||||
const hitFeat = unitsFeatures.get(atomicUnit.id);
|
||||
if (!hitFeat || hitFeat.count === 0) continue;
|
||||
|
||||
const infoHit = Features.Info(structure, atomicUnit, hitFeat);
|
||||
|
||||
const { indices: fIdxs, offsets: fOff } = hitFeat.elementsIndex;
|
||||
for (let k = fOff[hitLocalIdx], kl = fOff[hitLocalIdx + 1]; k < kl; k++) {
|
||||
const fi = fIdxs[k] as Features.FeatureIndex;
|
||||
const fType = hitFeat.types[fi];
|
||||
|
||||
if (fType !== FeatureType.HydrogenDonor && fType !== FeatureType.HydrogenAcceptor) continue;
|
||||
|
||||
const memberIdx = hitFeat.members[hitFeat.offsets[fi]] as StructureElement.UnitIndex;
|
||||
|
||||
if (!props.backbone && isBackboneAtom(atomicUnit, memberIdx)) continue;
|
||||
|
||||
atomicUnit.conformation.position(atomicUnit.elements[memberIdx], candidatePos);
|
||||
|
||||
const distSq = Vec3.squaredDistance(candidatePos, wPos);
|
||||
if (distSq < legDistMinSq || distSq > legDistMaxSq) continue;
|
||||
|
||||
infoHit.feature = fi;
|
||||
|
||||
if (fType === FeatureType.HydrogenDonor) {
|
||||
const key = featureKey(atomicUnit.id, fi);
|
||||
if (donorKeys.has(key)) continue;
|
||||
|
||||
if (checkGeometry(structure, infoHit, infoWAcc, legOpts)) {
|
||||
donorKeys.add(key);
|
||||
donors.push({
|
||||
unit: atomicUnit,
|
||||
featureIdx: fi,
|
||||
memberIdx,
|
||||
x: candidatePos[0],
|
||||
y: candidatePos[1],
|
||||
z: candidatePos[2],
|
||||
distSq,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const key = featureKey(atomicUnit.id, fi);
|
||||
if (acceptorKeys.has(key)) continue;
|
||||
|
||||
if (checkGeometry(structure, infoWDon, infoHit, legOpts)) {
|
||||
acceptorKeys.add(key);
|
||||
acceptors.push({
|
||||
unit: atomicUnit,
|
||||
featureIdx: fi,
|
||||
memberIdx,
|
||||
x: candidatePos[0],
|
||||
y: candidatePos[1],
|
||||
z: candidatePos[2],
|
||||
distSq,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const don of donors) {
|
||||
for (const acc of acceptors) {
|
||||
// Reject bridges where donor and acceptor are the same physical atom
|
||||
// represented by different feature indices.
|
||||
if (don.unit === acc.unit && don.memberIdx === acc.memberIdx) continue;
|
||||
|
||||
if (!checkOmega(don, wPos, acc, cosOmegaMin, cosOmegaMax)) continue;
|
||||
|
||||
const combinedDistSq = don.distSq + acc.distSq;
|
||||
const donorKey = featureKey(don.unit.id, don.featureIdx);
|
||||
const acceptorKey = featureKey(acc.unit.id, acc.featureIdx);
|
||||
|
||||
const existing = getBestBridge(best, donorKey, acceptorKey);
|
||||
if (!existing || combinedDistSq < existing.combinedDistSq) {
|
||||
setBestBridge(best, donorKey, acceptorKey, {
|
||||
contact: {
|
||||
unitA: don.unit.id,
|
||||
indexA: don.featureIdx,
|
||||
unitB: acc.unit.id,
|
||||
indexB: acc.featureIdx,
|
||||
unitM: unitW.id,
|
||||
indexMA: accFW,
|
||||
indexMB: donFW,
|
||||
props: { type: InteractionType.WaterBridge, flag: InteractionFlag.None },
|
||||
},
|
||||
combinedDistSq,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestBridgeValues(best).map(e => e.contact);
|
||||
}
|
||||
@@ -1,400 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sebastian Bittrich <sebastian.m.bittrich@gmail.com>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { VisualContext } from '../../../mol-repr/visual';
|
||||
import { Structure, StructureElement, Unit } from '../../../mol-model/structure';
|
||||
import { Theme } from '../../../mol-theme/theme';
|
||||
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { createLinkCylinderMesh, LinkCylinderParams, LinkStyle } from '../../../mol-repr/structure/visual/util/link';
|
||||
import { ComplexMeshParams, ComplexVisual, ComplexMeshVisual } from '../../../mol-repr/structure/complex-visual';
|
||||
import { VisualUpdateState } from '../../../mol-repr/util';
|
||||
import { PickingId } from '../../../mol-geo/geometry/picking';
|
||||
import { EmptyLoci, Loci } from '../../../mol-model/loci';
|
||||
import { NullLocation } from '../../../mol-model/location';
|
||||
import { Interval, OrderedSet } from '../../../mol-data/int';
|
||||
import { InteractionsProvider } from '../interactions';
|
||||
import { LocationIterator } from '../../../mol-geo/util/location-iterator';
|
||||
import { BridgeContacts, Bridges } from '../interactions/interactions';
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { InteractionsSharedParams } from './shared';
|
||||
import { Features } from '../interactions/features';
|
||||
|
||||
type CanonicalLegIndices = {
|
||||
endpointA: Int32Array
|
||||
endpointB: Int32Array
|
||||
};
|
||||
|
||||
const CanonicalLegIndicesCache = new WeakMap<BridgeContacts, CanonicalLegIndices>();
|
||||
|
||||
function getCanonicalLegIndices(bridges: BridgeContacts): CanonicalLegIndices {
|
||||
const cached = CanonicalLegIndicesCache.get(bridges);
|
||||
if (cached) return cached;
|
||||
|
||||
const n = bridges.length;
|
||||
const endpointA = new Int32Array(n);
|
||||
const endpointB = new Int32Array(n);
|
||||
|
||||
const legA = new Map<string, number>();
|
||||
const legB = new Map<string, number>();
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const b = bridges[i];
|
||||
|
||||
const kA = `${b.unitA}|${b.indexA}|${b.unitM}|${b.indexMA}`;
|
||||
const kB = `${b.unitM}|${b.indexMB}|${b.unitB}|${b.indexB}`;
|
||||
|
||||
let ai = legA.get(kA);
|
||||
if (ai === undefined) {
|
||||
ai = i;
|
||||
legA.set(kA, i);
|
||||
}
|
||||
endpointA[i] = ai;
|
||||
|
||||
let bi = legB.get(kB);
|
||||
if (bi === undefined) {
|
||||
bi = i;
|
||||
legB.set(kB, i);
|
||||
}
|
||||
endpointB[i] = bi;
|
||||
}
|
||||
|
||||
const indices = { endpointA, endpointB };
|
||||
CanonicalLegIndicesCache.set(bridges, indices);
|
||||
return indices;
|
||||
}
|
||||
|
||||
function getFeatureMember(features: Features, featureIndex: Features.FeatureIndex): StructureElement.UnitIndex {
|
||||
return features.members[features.offsets[featureIndex]] as StructureElement.UnitIndex;
|
||||
}
|
||||
|
||||
function atomPosition(unit: Unit.Atomic, features: Features, featureIndex: Features.FeatureIndex, out: Vec3) {
|
||||
const atomLocalIdx = getFeatureMember(features, featureIndex);
|
||||
unit.conformation.position(unit.elements[atomLocalIdx], out);
|
||||
}
|
||||
|
||||
function setFeatureLocation(
|
||||
structure: Structure,
|
||||
location: StructureElement.Location,
|
||||
unitId: number,
|
||||
features: Features,
|
||||
featureIndex: Features.FeatureIndex
|
||||
) {
|
||||
const unit = structure.unitMap.get(unitId) as Unit.Atomic;
|
||||
const atomLocalIdx = getFeatureMember(features, featureIndex);
|
||||
|
||||
location.unit = unit;
|
||||
location.element = unit.elements[atomLocalIdx];
|
||||
}
|
||||
|
||||
function applyLegA(
|
||||
bridgeIndex: number,
|
||||
bridgeCount: number,
|
||||
canonical: CanonicalLegIndices,
|
||||
apply: (interval: Interval) => boolean
|
||||
) {
|
||||
let changed = false;
|
||||
const i = canonical.endpointA[bridgeIndex];
|
||||
|
||||
if (apply(Interval.ofSingleton(i))) changed = true;
|
||||
if (apply(Interval.ofSingleton(i + bridgeCount))) changed = true;
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
function applyLegB(
|
||||
bridgeIndex: number,
|
||||
bridgeCount: number,
|
||||
canonical: CanonicalLegIndices,
|
||||
apply: (interval: Interval) => boolean
|
||||
) {
|
||||
let changed = false;
|
||||
const i = canonical.endpointB[bridgeIndex];
|
||||
|
||||
if (apply(Interval.ofSingleton(i + 2 * bridgeCount))) changed = true;
|
||||
if (apply(Interval.ofSingleton(i + 3 * bridgeCount))) changed = true;
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
function createBridgeCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<BridgeParams>, mesh?: Mesh) {
|
||||
if (!structure.hasAtomic) return Mesh.createEmpty(mesh);
|
||||
|
||||
const interactions = InteractionsProvider.get(structure).value;
|
||||
if (!interactions) return Mesh.createEmpty(mesh);
|
||||
|
||||
const { bridges, unitsFeatures } = interactions;
|
||||
|
||||
const n = bridges.length;
|
||||
if (!n) return Mesh.createEmpty(mesh);
|
||||
|
||||
const l = StructureElement.Location.create(structure);
|
||||
const { sizeFactor } = props;
|
||||
const canonical = getCanonicalLegIndices(bridges);
|
||||
|
||||
const builderProps = {
|
||||
// Four half-cylinders per bridge; createLinkCylinderMesh draws the A-side half per call:
|
||||
// [0, n): A→mediator, forward (A side)
|
||||
// [n, 2n): A→mediator, backward (mediator side)
|
||||
// [2n, 3n): mediator→B, forward (mediator side)
|
||||
// [3n, 4n): mediator→B, backward (B side)
|
||||
//
|
||||
// When multiple bridges share the same physical leg, only the first
|
||||
// occurrence is drawn; later ones map back to the canonical edge index.
|
||||
linkCount: 4 * n,
|
||||
|
||||
position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
|
||||
const b = bridges[edgeIndex % n];
|
||||
const uM = structure.unitMap.get(b.unitM) as Unit.Atomic;
|
||||
const fM = unitsFeatures.get(b.unitM);
|
||||
const leg = Math.floor(edgeIndex / n);
|
||||
|
||||
if (leg === 0) {
|
||||
const uA = structure.unitMap.get(b.unitA) as Unit.Atomic;
|
||||
const fA = unitsFeatures.get(b.unitA);
|
||||
atomPosition(uA, fA, b.indexA, posA);
|
||||
atomPosition(uM, fM, b.indexMA, posB);
|
||||
} else if (leg === 1) {
|
||||
const uA = structure.unitMap.get(b.unitA) as Unit.Atomic;
|
||||
const fA = unitsFeatures.get(b.unitA);
|
||||
atomPosition(uM, fM, b.indexMA, posA);
|
||||
atomPosition(uA, fA, b.indexA, posB);
|
||||
} else if (leg === 2) {
|
||||
const uB = structure.unitMap.get(b.unitB) as Unit.Atomic;
|
||||
const fB = unitsFeatures.get(b.unitB);
|
||||
atomPosition(uM, fM, b.indexMB, posA);
|
||||
atomPosition(uB, fB, b.indexB, posB);
|
||||
} else {
|
||||
const uB = structure.unitMap.get(b.unitB) as Unit.Atomic;
|
||||
const fB = unitsFeatures.get(b.unitB);
|
||||
atomPosition(uB, fB, b.indexB, posA);
|
||||
atomPosition(uM, fM, b.indexMB, posB);
|
||||
}
|
||||
},
|
||||
|
||||
ignore: (edgeIndex: number) => {
|
||||
const bi = edgeIndex % n;
|
||||
const leg = Math.floor(edgeIndex / n);
|
||||
|
||||
return leg <= 1
|
||||
? canonical.endpointA[bi] !== bi
|
||||
: canonical.endpointB[bi] !== bi;
|
||||
},
|
||||
|
||||
style: (_edgeIndex: number) => LinkStyle.Dashed,
|
||||
|
||||
radius: (edgeIndex: number) => {
|
||||
const b = bridges[edgeIndex % n];
|
||||
const leg = Math.floor(edgeIndex / n);
|
||||
const isLegA = leg <= 1;
|
||||
|
||||
if (isLegA) {
|
||||
const fA = unitsFeatures.get(b.unitA);
|
||||
const fM = unitsFeatures.get(b.unitM);
|
||||
|
||||
setFeatureLocation(structure, l, b.unitA, fA, b.indexA);
|
||||
const sizeA = theme.size.size(l);
|
||||
|
||||
setFeatureLocation(structure, l, b.unitM, fM, b.indexMA);
|
||||
const sizeM = theme.size.size(l);
|
||||
|
||||
return Math.min(sizeA, sizeM) * sizeFactor;
|
||||
} else {
|
||||
const fM = unitsFeatures.get(b.unitM);
|
||||
const fB = unitsFeatures.get(b.unitB);
|
||||
|
||||
setFeatureLocation(structure, l, b.unitM, fM, b.indexMB);
|
||||
const sizeM = theme.size.size(l);
|
||||
|
||||
setFeatureLocation(structure, l, b.unitB, fB, b.indexB);
|
||||
const sizeB = theme.size.size(l);
|
||||
|
||||
return Math.min(sizeM, sizeB) * sizeFactor;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const { mesh: m, boundingSphere } = createLinkCylinderMesh(ctx, builderProps, props, mesh);
|
||||
|
||||
if (boundingSphere) {
|
||||
m.setBoundingSphere(boundingSphere);
|
||||
} else if (m.triangleCount > 0) {
|
||||
const sphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, sizeFactor);
|
||||
m.setBoundingSphere(sphere);
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
export const BridgeParams = {
|
||||
...ComplexMeshParams,
|
||||
...LinkCylinderParams,
|
||||
...InteractionsSharedParams,
|
||||
};
|
||||
export type BridgeParams = typeof BridgeParams
|
||||
|
||||
export function BridgeVisual(materialId: number): ComplexVisual<BridgeParams> {
|
||||
return ComplexMeshVisual<BridgeParams>({
|
||||
defaultProps: PD.getDefaultValues(BridgeParams),
|
||||
createGeometry: createBridgeCylinderMesh,
|
||||
createLocationIterator: createBridgeIterator,
|
||||
getLoci: getBridgeLoci,
|
||||
eachLocation: eachBridgeInteraction,
|
||||
|
||||
setUpdateState: (
|
||||
state: VisualUpdateState,
|
||||
newProps: PD.Values<BridgeParams>,
|
||||
currentProps: PD.Values<BridgeParams>,
|
||||
newTheme: Theme,
|
||||
currentTheme: Theme,
|
||||
newStructure: Structure,
|
||||
_currentStructure: Structure
|
||||
) => {
|
||||
state.createGeometry = (
|
||||
newProps.sizeFactor !== currentProps.sizeFactor ||
|
||||
newProps.dashCount !== currentProps.dashCount ||
|
||||
newProps.dashScale !== currentProps.dashScale ||
|
||||
newProps.dashCap !== currentProps.dashCap ||
|
||||
newProps.radialSegments !== currentProps.radialSegments ||
|
||||
newTheme.size !== currentTheme.size
|
||||
);
|
||||
|
||||
const interactionsHash = InteractionsProvider.get(newStructure).version;
|
||||
if ((state.info.interactionsHash as number) !== interactionsHash) {
|
||||
state.createGeometry = true;
|
||||
state.updateTransform = true;
|
||||
state.updateColor = true;
|
||||
state.info.interactionsHash = interactionsHash;
|
||||
}
|
||||
}
|
||||
}, materialId);
|
||||
}
|
||||
|
||||
function getBridgeLoci(pickingId: PickingId, structure: Structure, id: number) {
|
||||
const { objectId, groupId } = pickingId;
|
||||
if (id !== objectId) return EmptyLoci;
|
||||
|
||||
const interactions = InteractionsProvider.get(structure).value;
|
||||
if (!interactions) return EmptyLoci;
|
||||
|
||||
const { bridges, unitsFeatures } = interactions;
|
||||
const n = bridges.length;
|
||||
|
||||
if (!n || groupId < 0 || groupId >= 4 * n) return EmptyLoci;
|
||||
|
||||
const bridgeIndex = groupId % n;
|
||||
|
||||
return Bridges.Loci({ structure, bridges, unitsFeatures }, [{ bridgeIndex }]);
|
||||
}
|
||||
|
||||
const __unitMap = new Map<number, OrderedSet<StructureElement.UnitIndex>>();
|
||||
|
||||
function eachBridgeInteraction(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean, _isMarking: boolean) {
|
||||
let changed = false;
|
||||
|
||||
if (Bridges.isLoci(loci)) {
|
||||
if (!Structure.areEquivalent(loci.data.structure, structure)) return false;
|
||||
|
||||
const interactions = InteractionsProvider.get(structure).value;
|
||||
if (!interactions) return false;
|
||||
|
||||
const { bridges } = interactions;
|
||||
const n = bridges.length;
|
||||
if (!n) return false;
|
||||
|
||||
const canonical = getCanonicalLegIndices(bridges);
|
||||
|
||||
for (const e of loci.elements) {
|
||||
if (e.bridgeIndex < 0 || e.bridgeIndex >= n) continue;
|
||||
|
||||
if (applyLegA(e.bridgeIndex, n, canonical, apply)) changed = true;
|
||||
if (applyLegB(e.bridgeIndex, n, canonical, apply)) changed = true;
|
||||
}
|
||||
} else if (StructureElement.Loci.is(loci)) {
|
||||
if (!Structure.areEquivalent(loci.structure, structure)) return false;
|
||||
|
||||
const interactions = InteractionsProvider.get(structure).value;
|
||||
if (!interactions) return false;
|
||||
|
||||
const { bridges, unitsFeatures } = interactions;
|
||||
const n = bridges.length;
|
||||
if (!n) return false;
|
||||
|
||||
const canonical = getCanonicalLegIndices(bridges);
|
||||
|
||||
__unitMap.clear();
|
||||
for (const e of loci.elements) {
|
||||
__unitMap.set(e.unit.id, e.indices);
|
||||
}
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const b = bridges[i];
|
||||
|
||||
const indicesA = __unitMap.get(b.unitA);
|
||||
const indicesM = __unitMap.get(b.unitM);
|
||||
const indicesB = __unitMap.get(b.unitB);
|
||||
|
||||
if (!indicesA && !indicesM && !indicesB) continue;
|
||||
|
||||
let hitA = false;
|
||||
if (indicesA) {
|
||||
const fA = unitsFeatures.get(b.unitA);
|
||||
const mi = getFeatureMember(fA, b.indexA);
|
||||
hitA = OrderedSet.has(indicesA, mi);
|
||||
}
|
||||
|
||||
let hitM = false;
|
||||
if (indicesM) {
|
||||
const fM = unitsFeatures.get(b.unitM);
|
||||
const miA = getFeatureMember(fM, b.indexMA);
|
||||
const miB = getFeatureMember(fM, b.indexMB);
|
||||
hitM = OrderedSet.has(indicesM, miA) || OrderedSet.has(indicesM, miB);
|
||||
}
|
||||
|
||||
let hitB = false;
|
||||
if (indicesB) {
|
||||
const fB = unitsFeatures.get(b.unitB);
|
||||
const mi = getFeatureMember(fB, b.indexB);
|
||||
hitB = OrderedSet.has(indicesB, mi);
|
||||
}
|
||||
|
||||
if (hitA || hitM) {
|
||||
if (applyLegA(i, n, canonical, apply)) changed = true;
|
||||
}
|
||||
|
||||
if (hitB || hitM) {
|
||||
if (applyLegB(i, n, canonical, apply)) changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
__unitMap.clear();
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
function createBridgeIterator(structure: Structure): LocationIterator {
|
||||
const interactions = InteractionsProvider.get(structure).value;
|
||||
if (!interactions) return LocationIterator(0, 1, 1, () => NullLocation, true);
|
||||
|
||||
const { bridges, unitsFeatures } = interactions;
|
||||
|
||||
const n = bridges.length;
|
||||
const groupCount = 4 * n;
|
||||
const instanceCount = 1;
|
||||
|
||||
const data: Bridges.Data = { structure, bridges, unitsFeatures };
|
||||
const location = Bridges.Location(data);
|
||||
const { element } = location;
|
||||
|
||||
const getLocation = (groupIndex: number) => {
|
||||
element.bridgeIndex = n === 0 ? 0 : groupIndex % n;
|
||||
return location;
|
||||
};
|
||||
|
||||
return LocationIterator(groupCount, instanceCount, 1, getLocation, true);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -12,23 +12,20 @@ import { UnitsRepresentation, StructureRepresentation, StructureRepresentationSt
|
||||
import { InteractionsIntraUnitParams, InteractionsIntraUnitVisual } from './interactions-intra-unit-cylinder';
|
||||
import { InteractionsProvider } from '../interactions';
|
||||
import { InteractionsInterUnitParams, InteractionsInterUnitVisual } from './interactions-inter-unit-cylinder';
|
||||
import { BridgeParams, BridgeVisual } from './interactions-bridge-cylinder';
|
||||
import { CustomProperty } from '../../common/custom-property';
|
||||
import { getUnitKindsParam } from '../../../mol-repr/structure/params';
|
||||
|
||||
const InteractionsVisuals = {
|
||||
'intra-unit': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InteractionsIntraUnitParams>) => UnitsRepresentation('Intra-unit interactions cylinder', ctx, getParams, InteractionsIntraUnitVisual),
|
||||
'inter-unit': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InteractionsInterUnitParams>) => ComplexRepresentation('Inter-unit interactions cylinder', ctx, getParams, InteractionsInterUnitVisual),
|
||||
'bridge': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, BridgeParams>) => ComplexRepresentation('Bridge cylinder', ctx, getParams, BridgeVisual),
|
||||
};
|
||||
|
||||
export const InteractionsParams = {
|
||||
...InteractionsIntraUnitParams,
|
||||
...InteractionsInterUnitParams,
|
||||
...BridgeParams,
|
||||
unitKinds: getUnitKindsParam(['atomic']),
|
||||
sizeFactor: PD.Numeric(0.2, { min: 0.01, max: 1, step: 0.01 }),
|
||||
visuals: PD.MultiSelect(['intra-unit', 'inter-unit', 'bridge'], PD.objectToOptions(InteractionsVisuals)),
|
||||
visuals: PD.MultiSelect(['intra-unit', 'inter-unit'], PD.objectToOptions(InteractionsVisuals)),
|
||||
};
|
||||
export type InteractionsParams = typeof InteractionsParams
|
||||
export function getInteractionParams(ctx: ThemeRegistryContext, structure: Structure) {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Sebastian Bittrich <sebastian.m.bittrich@gmail.com>
|
||||
*/
|
||||
|
||||
import { Location } from '../../../mol-model/location';
|
||||
@@ -13,7 +12,7 @@ import { ThemeDataContext } from '../../../mol-theme/theme';
|
||||
import { ColorTheme, LocationColor } from '../../../mol-theme/color';
|
||||
import { InteractionType } from '../interactions/common';
|
||||
import { TableLegend } from '../../../mol-util/legend';
|
||||
import { Interactions, Bridges } from '../interactions/interactions';
|
||||
import { Interactions } from '../interactions/interactions';
|
||||
import { CustomProperty } from '../../common/custom-property';
|
||||
import { hash2 } from '../../../mol-data/util';
|
||||
import { ColorThemeCategory } from '../../../mol-theme/color/categories';
|
||||
@@ -30,7 +29,6 @@ const InteractionTypeColors = ColorMap({
|
||||
CationPi: 0xFF8000,
|
||||
PiStacking: 0x8CB366,
|
||||
WeakHydrogenBond: 0xC5DDEC,
|
||||
WaterBridge: 0x00CCEE,
|
||||
});
|
||||
|
||||
const InteractionTypeColorTable: [string, Color][] = [
|
||||
@@ -42,7 +40,6 @@ const InteractionTypeColorTable: [string, Color][] = [
|
||||
['Cation Pi', InteractionTypeColors.CationPi],
|
||||
['Pi Stacking', InteractionTypeColors.PiStacking],
|
||||
['Weak HydrogenBond', InteractionTypeColors.WeakHydrogenBond],
|
||||
['Water Bridge', InteractionTypeColors.WaterBridge],
|
||||
];
|
||||
|
||||
function typeColor(type: InteractionType): Color {
|
||||
@@ -63,8 +60,6 @@ function typeColor(type: InteractionType): Color {
|
||||
return InteractionTypeColors.PiStacking;
|
||||
case InteractionType.WeakHydrogenBond:
|
||||
return InteractionTypeColors.WeakHydrogenBond;
|
||||
case InteractionType.WaterBridge:
|
||||
return InteractionTypeColors.WaterBridge;
|
||||
case InteractionType.Unknown:
|
||||
return DefaultColor;
|
||||
}
|
||||
@@ -96,9 +91,6 @@ export function InteractionTypeColorTheme(ctx: ThemeDataContext, props: PD.Value
|
||||
return typeColor(contacts.edges[idx].props.type);
|
||||
}
|
||||
}
|
||||
if (Bridges.isLocation(location)) {
|
||||
return typeColor(location.data.bridges[location.element.bridgeIndex].props.type);
|
||||
}
|
||||
return DefaultColor;
|
||||
};
|
||||
} else {
|
||||
|
||||
@@ -167,9 +167,9 @@ namespace Loci {
|
||||
} else if (loci.kind === 'data-loci') {
|
||||
return loci.getBoundingSphere?.(boundingSphere);
|
||||
} else if (loci.kind === 'volume-loci') {
|
||||
return Volume.getBoundingSphere(loci.volume, loci.instances, boundingSphere);
|
||||
return Volume.getBoundingSphere(loci.volume, boundingSphere);
|
||||
} else if (loci.kind === 'isosurface-loci') {
|
||||
return Volume.Isosurface.getBoundingSphere(loci.volume, loci.isoValue, loci.instances, boundingSphere);
|
||||
return Volume.Isosurface.getBoundingSphere(loci.volume, loci.isoValue, boundingSphere);
|
||||
} else if (loci.kind === 'cell-loci') {
|
||||
return Volume.Cell.getBoundingSphere(loci.volume, loci.elements, boundingSphere);
|
||||
} else if (loci.kind === 'segment-loci') {
|
||||
|
||||
@@ -545,12 +545,6 @@ export function surroundingLigands({ query, radius, includeWater }: SurroundingL
|
||||
continue;
|
||||
}
|
||||
|
||||
// Water is handled exclusively by the `includeWater` 3D-lookup branch below.
|
||||
// A single water pulled in via a struct_conn metalc/covale edge would
|
||||
// otherwise match every other water in the chain (all share label_seq_id
|
||||
// and label_comp_id) and leak the entire chain.
|
||||
if (StructureProperties.entity.type(l) === 'water') continue;
|
||||
|
||||
residuesIt.setSegment(chainSegment);
|
||||
while (residuesIt.hasNext) {
|
||||
const residueSegment = residuesIt.move();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -95,22 +95,10 @@ namespace UnitRing {
|
||||
Elements.SN, Elements.SB,
|
||||
Elements.BI
|
||||
] as ElementSymbol[]);
|
||||
/**
|
||||
* Elements that are sp3 (and therefore non-aromatic) when degree >= 4 with no pi bonds.
|
||||
* Excludes O (never realistically reaches degree 4) and N (quaternary N can be aromatic,
|
||||
* but is guarded by the hasPiBond check below).
|
||||
*/
|
||||
const Sp3RingCheckElements = new Set([
|
||||
Elements.B, Elements.C, Elements.N,
|
||||
Elements.SI, Elements.P, Elements.S,
|
||||
Elements.GE, Elements.AS,
|
||||
Elements.SN, Elements.SB,
|
||||
Elements.BI
|
||||
] as ElementSymbol[]);
|
||||
const AromaticRingPlanarityThreshold = 0.05;
|
||||
|
||||
export function isAromatic(unit: Unit.Atomic, ring: UnitRing): boolean {
|
||||
const { elements, bonds: { b, offset, edgeProps: { flags, order } } } = unit;
|
||||
const { elements, bonds: { b, offset, edgeProps: { flags } } } = unit;
|
||||
const { type_symbol, label_comp_id } = unit.model.atomicHierarchy.atoms;
|
||||
|
||||
// ignore Proline (can be flat because of bad geometry)
|
||||
@@ -132,25 +120,6 @@ namespace UnitRing {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0, il = ring.length; i < il; ++i) {
|
||||
const aI = ring[i];
|
||||
const elem = type_symbol.value(elements[aI]);
|
||||
if (!Sp3RingCheckElements.has(elem)) continue;
|
||||
|
||||
let degree = 0;
|
||||
let hasPiBond = false;
|
||||
for (let j = offset[aI], jl = offset[aI + 1]; j < jl; ++j) {
|
||||
degree += 1;
|
||||
const f = flags[j];
|
||||
const o = order[j];
|
||||
if (BondType.is(BondType.Flag.Aromatic, f) || o === 2 || o === 3) {
|
||||
hasPiBond = true;
|
||||
}
|
||||
}
|
||||
if (degree >= 4 && !hasPiBond) return false;
|
||||
}
|
||||
|
||||
if (aromaticBondCount === 2 * ring.length) return true;
|
||||
if (!hasAromaticRingElement) return false;
|
||||
if (ring.length < 5) return false;
|
||||
|
||||
@@ -68,36 +68,6 @@ namespace Grid {
|
||||
return Sphere3D.fromDimensionsAndTransform(boundingSphere, dimensions, transform);
|
||||
}
|
||||
|
||||
const _isoBbox = Box3D();
|
||||
export function getIsosurfaceBoundingSphere(grid: Grid, isoValue: number, boundingSphere?: Sphere3D) {
|
||||
const neg = isoValue < 0;
|
||||
|
||||
const c = [0, 0, 0];
|
||||
const getCoords = grid.cells.space.getCoords;
|
||||
const d = grid.cells.data;
|
||||
const [xn, yn, zn] = grid.cells.space.dimensions;
|
||||
|
||||
let minx = xn - 1, miny = yn - 1, minz = zn - 1;
|
||||
let maxx = 0, maxy = 0, maxz = 0;
|
||||
for (let i = 0, il = d.length; i < il; ++i) {
|
||||
if ((neg && d[i] <= isoValue) || (!neg && d[i] >= isoValue)) {
|
||||
getCoords(i, c);
|
||||
if (c[0] < minx) minx = c[0];
|
||||
if (c[1] < miny) miny = c[1];
|
||||
if (c[2] < minz) minz = c[2];
|
||||
if (c[0] > maxx) maxx = c[0];
|
||||
if (c[1] > maxy) maxy = c[1];
|
||||
if (c[2] > maxz) maxz = c[2];
|
||||
}
|
||||
}
|
||||
|
||||
Vec3.set(_isoBbox.min, minx - 1, miny - 1, minz - 1);
|
||||
Vec3.set(_isoBbox.max, maxx + 1, maxy + 1, maxz + 1);
|
||||
const transform = Grid.getGridToCartesianTransform(grid);
|
||||
Box3D.transform(_isoBbox, _isoBbox, transform);
|
||||
return Sphere3D.fromBox3D(boundingSphere || Sphere3D(), _isoBbox);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute histogram with given bin count.
|
||||
* Cached on the Grid object.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import { Grid } from './grid';
|
||||
import { Interval, OrderedSet } from '../../mol-data/int';
|
||||
import { OrderedSet } from '../../mol-data/int';
|
||||
import { Box3D, Sphere3D } from '../../mol-math/geometry';
|
||||
import { Vec3, Mat4 } from '../../mol-math/linear-algebra';
|
||||
import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
|
||||
@@ -191,14 +191,14 @@ export namespace Volume {
|
||||
export function isLociEmpty(loci: Loci) { return isEmpty(loci.volume) || OrderedSet.isEmpty(loci.instances); }
|
||||
|
||||
const boundaryHelper = new BoundaryHelper('98');
|
||||
export function getBoundingSphere(volume: Volume, instances: OrderedSet<InstanceIndex>, boundingSphere?: Sphere3D) {
|
||||
export function getBoundingSphere(volume: Volume, boundingSphere?: Sphere3D) {
|
||||
const gs = Grid.getBoundingSphere(volume.grid);
|
||||
if (!boundingSphere) boundingSphere = Sphere3D();
|
||||
if (OrderedSet.isEmpty(instances)) return Sphere3D.copy(boundingSphere, gs);
|
||||
if (volume.instances.length === 0) return Sphere3D.copy(boundingSphere, gs);
|
||||
|
||||
const spheres: Sphere3D[] = [];
|
||||
for (let i = 0, il = OrderedSet.size(instances); i < il; ++i) {
|
||||
const { transform } = volume.instances[OrderedSet.getAt(instances, i)];
|
||||
for (let i = 0, il = volume.instances.length; i < il; ++i) {
|
||||
const { transform } = volume.instances[i];
|
||||
spheres.push(Sphere3D.transform(Sphere3D(), gs, transform));
|
||||
}
|
||||
|
||||
@@ -220,23 +220,35 @@ export namespace Volume {
|
||||
export function areLociEqual(a: Loci, b: Loci) { return a.volume === b.volume && Volume.IsoValue.areSame(a.isoValue, b.isoValue, a.volume.grid.stats) && OrderedSet.areEqual(a.instances, b.instances); }
|
||||
export function isLociEmpty(loci: Loci) { return isEmpty(loci.volume) || OrderedSet.isEmpty(loci.instances); }
|
||||
|
||||
const boundaryHelper = new BoundaryHelper('98');
|
||||
export function getBoundingSphere(volume: Volume, isoValue: Volume.IsoValue, instances: OrderedSet<InstanceIndex>, boundingSphere?: Sphere3D) {
|
||||
const bbox = Box3D();
|
||||
export function getBoundingSphere(volume: Volume, isoValue: Volume.IsoValue, boundingSphere?: Sphere3D) {
|
||||
const value = Volume.IsoValue.toAbsolute(isoValue, volume.grid.stats).absoluteValue;
|
||||
const gs = Grid.getIsosurfaceBoundingSphere(volume.grid, value);
|
||||
const neg = value < 0;
|
||||
|
||||
if (OrderedSet.isEmpty(instances)) return Sphere3D.copy(boundingSphere || Sphere3D(), gs);
|
||||
const c = [0, 0, 0];
|
||||
const getCoords = volume.grid.cells.space.getCoords;
|
||||
const d = volume.grid.cells.data;
|
||||
const [xn, yn, zn] = volume.grid.cells.space.dimensions;
|
||||
|
||||
const spheres: Sphere3D[] = [];
|
||||
for (let i = 0, il = OrderedSet.size(instances); i < il; ++i) {
|
||||
spheres.push(Sphere3D.transform(Sphere3D(), gs, volume.instances[OrderedSet.getAt(instances, i)].transform));
|
||||
let minx = xn - 1, miny = yn - 1, minz = zn - 1;
|
||||
let maxx = 0, maxy = 0, maxz = 0;
|
||||
for (let i = 0, il = d.length; i < il; ++i) {
|
||||
if ((neg && d[i] <= value) || (!neg && d[i] >= value)) {
|
||||
getCoords(i, c);
|
||||
if (c[0] < minx) minx = c[0];
|
||||
if (c[1] < miny) miny = c[1];
|
||||
if (c[2] < minz) minz = c[2];
|
||||
if (c[0] > maxx) maxx = c[0];
|
||||
if (c[1] > maxy) maxy = c[1];
|
||||
if (c[2] > maxz) maxz = c[2];
|
||||
}
|
||||
}
|
||||
|
||||
boundaryHelper.reset();
|
||||
for (const s of spheres) boundaryHelper.includeSphere(s);
|
||||
boundaryHelper.finishedIncludeStep();
|
||||
for (const s of spheres) boundaryHelper.radiusSphere(s);
|
||||
return boundaryHelper.getSphere(boundingSphere);
|
||||
Vec3.set(bbox.min, minx - 1, miny - 1, minz - 1);
|
||||
Vec3.set(bbox.max, maxx + 1, maxy + 1, maxz + 1);
|
||||
const transform = Grid.getGridToCartesianTransform(volume.grid);
|
||||
Box3D.transform(bbox, bbox, transform);
|
||||
return Sphere3D.fromBox3D(boundingSphere || Sphere3D(), bbox);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,7 +416,7 @@ export namespace Volume {
|
||||
}
|
||||
return Sphere3D.fromBox3D(boundingSphere || Sphere3D(), bbox);
|
||||
} else {
|
||||
return Volume.getBoundingSphere(volume, Interval.ofLength(volume.instances.length as InstanceIndex), boundingSphere);
|
||||
return Volume.getBoundingSphere(volume, boundingSphere);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
@@ -77,10 +76,7 @@ const DownloadStructure = StateAction.build({
|
||||
}, { isFlat: true, label: 'SWISS-MODEL', description: 'Loads the best homology model or experimental structure' }),
|
||||
'alphafolddb': PD.Group({
|
||||
provider: PD.Group({
|
||||
id: PD.Text('Q8W3K0', {
|
||||
label: 'ID(s)',
|
||||
description: 'One or more comma/space separated IDs. Each ID can be either UniProt accession (e.g. Q14676, Q14676-2) or AlphaFoldDB model entity ID (e.g. AF-Q14676-F1, AF-Q14676-2-F1, AF-0000000066074510). Version suffixes (e.g. -v1) will be ignored and the newest model version will be downloaded.',
|
||||
}),
|
||||
id: PD.Text('Q8W3K0', { label: 'UniProtKB AC(s)', description: 'One or more comma/space separated ACs.' }),
|
||||
encoding: PD.Select('bcif', PD.arrayToOptions(['cif', 'bcif'] as const)),
|
||||
}, { pivot: 'id' }),
|
||||
options
|
||||
@@ -156,11 +152,7 @@ const DownloadStructure = StateAction.build({
|
||||
case 'alphafolddb':
|
||||
downloadParams = await getDownloadParams(src.params.provider.id,
|
||||
async id => {
|
||||
// id = UniProt accession: Q14676, Q14676-4
|
||||
// id = model entity ID: AF-Q14676-F1, AF-Q14676-4-F1, AF-0000000066074510
|
||||
// id = model entity ID + version to be ignored: AF-Q14676-4-F1-v6, AF-0000000066074510-v1
|
||||
const cleanId = id.replace(/-v\d+$/i, '').toUpperCase(); // Ignore version suffix (e.g. "-v6") because it is not a part of the ID, but displayed on AFDB page and people often copy-paste it
|
||||
const url = `https://www.alphafold.ebi.ac.uk/api/prediction/${cleanId}`;
|
||||
const url = `https://www.alphafold.ebi.ac.uk/api/prediction/${id.toUpperCase()}`;
|
||||
const info = await plugin.runTask(plugin.fetch({ url, type: 'json' }));
|
||||
if (Array.isArray(info) && info.length > 0) {
|
||||
const prop = src.params.provider.encoding === 'bcif' ? 'bcifUrl' : 'cifUrl';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -12,7 +12,7 @@ import { degToRad } from '../../../mol-math/misc';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { PluginStateAnimation } from '../model';
|
||||
|
||||
const _dir = Vec3(), _axis = Vec3(), _rot = Quat(), _up = Vec3(), _side = Vec3();
|
||||
const _dir = Vec3(), _axis = Vec3(), _rot = Quat();
|
||||
|
||||
type State = { snapshot: Camera.Snapshot };
|
||||
|
||||
@@ -24,7 +24,6 @@ export const AnimateCameraRock = PluginStateAnimation.create({
|
||||
durationInMs: PD.Numeric(4000, { min: 100, max: 20000, step: 100 }),
|
||||
speed: PD.Numeric(1, { min: 1, max: 10, step: 1 }, { description: 'How many times to rock from side to side.' }),
|
||||
angle: PD.Numeric(10, { min: 0, max: 180, step: 1 }, { description: 'How many degrees to rotate in each direction.' }),
|
||||
axis: PD.Vec3(Vec3.create(0, -1, 0), {}, { description: 'Axis of rotation in camera space' }),
|
||||
}),
|
||||
initialState: (p, ctx) => ({ snapshot: ctx.canvas3d!.camera.getSnapshot() }) as State,
|
||||
getDuration: p => ({ kind: 'fixed', durationMs: p.durationInMs }),
|
||||
@@ -48,25 +47,11 @@ export const AnimateCameraRock = PluginStateAnimation.create({
|
||||
const angle = Math.sin(phase * ctx.params.speed * Math.PI * 2) * degToRad(ctx.params.angle);
|
||||
|
||||
Vec3.sub(_dir, snapshot.position, snapshot.target);
|
||||
|
||||
// Transform axis from camera space to world space
|
||||
Vec3.normalize(_axis, _dir); // Z = view direction
|
||||
Vec3.normalize(_up, snapshot.up); // Y = up
|
||||
Vec3.cross(_side, _up, _axis); // X = right
|
||||
Vec3.normalize(_side, _side);
|
||||
const a = ctx.params.axis ?? Vec3.create(0, -1, 0); // default for backwards compatibility
|
||||
Vec3.set(_axis,
|
||||
a[0] * _side[0] + a[1] * _up[0] + a[2] * _axis[0],
|
||||
a[0] * _side[1] + a[1] * _up[1] + a[2] * _axis[1],
|
||||
a[0] * _side[2] + a[1] * _up[2] + a[2] * _axis[2]
|
||||
);
|
||||
Vec3.normalize(_axis, _axis);
|
||||
|
||||
Vec3.normalize(_axis, snapshot.up);
|
||||
Quat.setAxisAngle(_rot, _axis, angle);
|
||||
Vec3.transformQuat(_dir, _dir, _rot);
|
||||
Vec3.transformQuat(_up, snapshot.up, _rot);
|
||||
const position = Vec3.add(Vec3(), snapshot.target, _dir);
|
||||
ctx.plugin.canvas3d?.requestCameraReset({ snapshot: { ...snapshot, position, up: _up }, durationMs: 0 });
|
||||
ctx.plugin.canvas3d?.requestCameraReset({ snapshot: { ...snapshot, position }, durationMs: 0 });
|
||||
|
||||
if (phase >= 0.99999) {
|
||||
return { kind: 'finished' };
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Camera } from '../../../mol-canvas3d/camera';
|
||||
@@ -12,7 +11,7 @@ import { Vec3 } from '../../../mol-math/linear-algebra/3d/vec3';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { PluginStateAnimation } from '../model';
|
||||
|
||||
const _dir = Vec3(), _axis = Vec3(), _rot = Quat(), _up = Vec3(), _side = Vec3();
|
||||
const _dir = Vec3(), _axis = Vec3(), _rot = Quat();
|
||||
|
||||
type State = { snapshot: Camera.Snapshot };
|
||||
|
||||
@@ -23,7 +22,7 @@ export const AnimateCameraSpin = PluginStateAnimation.create({
|
||||
params: () => ({
|
||||
durationInMs: PD.Numeric(4000, { min: 100, max: 20000, step: 100 }),
|
||||
speed: PD.Numeric(1, { min: 1, max: 10, step: 1 }, { description: 'How many times to spin in the specified duration.' }),
|
||||
axis: PD.Vec3(Vec3.create(0, -1, 0), {}, { description: 'Axis of rotation in camera space' }),
|
||||
direction: PD.Select<'cw' | 'ccw'>('cw', [['cw', 'Clockwise'], ['ccw', 'Counter Clockwise']], { cycle: true })
|
||||
}),
|
||||
initialState: (_, ctx) => ({ snapshot: ctx.canvas3d?.camera.getSnapshot()! }) as State,
|
||||
getDuration: p => ({ kind: 'fixed', durationMs: p.durationInMs }),
|
||||
@@ -43,28 +42,14 @@ export const AnimateCameraSpin = PluginStateAnimation.create({
|
||||
const phase = t.animation
|
||||
? t.animation?.currentFrame / (t.animation.frameCount + 1)
|
||||
: clamp(t.current / ctx.params.durationInMs, 0, 1);
|
||||
const angle = 2 * Math.PI * phase * ctx.params.speed;
|
||||
const angle = 2 * Math.PI * phase * ctx.params.speed * (ctx.params.direction === 'ccw' ? -1 : 1);
|
||||
|
||||
Vec3.sub(_dir, snapshot.position, snapshot.target);
|
||||
|
||||
// Transform axis from camera space to world space
|
||||
Vec3.normalize(_axis, _dir); // Z = view direction
|
||||
Vec3.normalize(_up, snapshot.up); // Y = up
|
||||
Vec3.cross(_side, _up, _axis); // X = right
|
||||
Vec3.normalize(_side, _side);
|
||||
const a = ctx.params.axis ?? Vec3.create(0, -1, 0); // default for backwards compatibility
|
||||
Vec3.set(_axis,
|
||||
a[0] * _side[0] + a[1] * _up[0] + a[2] * _axis[0],
|
||||
a[0] * _side[1] + a[1] * _up[1] + a[2] * _axis[1],
|
||||
a[0] * _side[2] + a[1] * _up[2] + a[2] * _axis[2]
|
||||
);
|
||||
Vec3.normalize(_axis, _axis);
|
||||
|
||||
Vec3.normalize(_axis, snapshot.up);
|
||||
Quat.setAxisAngle(_rot, _axis, angle);
|
||||
Vec3.transformQuat(_dir, _dir, _rot);
|
||||
Vec3.transformQuat(_up, snapshot.up, _rot);
|
||||
const position = Vec3.add(Vec3(), snapshot.target, _dir);
|
||||
ctx.plugin.canvas3d?.requestCameraReset({ snapshot: { ...snapshot, position, up: _up }, durationMs: 0 });
|
||||
ctx.plugin.canvas3d?.requestCameraReset({ snapshot: { ...snapshot, position }, durationMs: 0 });
|
||||
|
||||
if (phase >= 0.99999) {
|
||||
return { kind: 'finished' };
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Ludovic Autin <autin@scripps.edu>
|
||||
*/
|
||||
|
||||
import { CustomProperties } from '../../../mol-model/custom-property';
|
||||
import { Grid, Volume } from '../../../mol-model/volume';
|
||||
import { Mat4, Tensor } from '../../../mol-math/linear-algebra';
|
||||
import { createVolumeSphereImpostor } from '../dot';
|
||||
|
||||
function createTestVolume(dimensions: [number, number, number], data: number[]): Volume {
|
||||
return {
|
||||
grid: {
|
||||
transform: { kind: 'matrix', matrix: Mat4.identity() },
|
||||
cells: Tensor.create(Tensor.Space(dimensions, [2, 1, 0]), Tensor.Data1(data)),
|
||||
stats: { min: 0, max: 1, mean: 0.5, sigma: 0.5 },
|
||||
} satisfies Grid,
|
||||
instances: [{ transform: Mat4.identity() }],
|
||||
sourceData: { kind: 'test', name: 'test', data: {} } as any,
|
||||
customProperties: new CustomProperties(),
|
||||
_propertyData: Object.create(null),
|
||||
_localPropertyData: Object.create(null),
|
||||
};
|
||||
}
|
||||
|
||||
describe('volume dot representation', () => {
|
||||
it('adds sphere impostor dots in Morton order for LOD sampling', () => {
|
||||
const volume = createTestVolume([2, 2, 2], [
|
||||
1, 1,
|
||||
1, 1,
|
||||
1, 1,
|
||||
1, 1,
|
||||
]);
|
||||
const spheres = createVolumeSphereImpostor(undefined as any, volume, 0, undefined as any, {
|
||||
isoValue: Volume.IsoValue.absolute(0.5),
|
||||
perturbPositions: false,
|
||||
lodLevels: [{ minDistance: 0, maxDistance: 0, overlap: 0, stride: 0, scaleBias: 3 }],
|
||||
} as any);
|
||||
|
||||
expect(Array.from(spheres.groupBuffer.ref.value)).toEqual([0, 4, 2, 6, 1, 5, 3, 7]);
|
||||
});
|
||||
|
||||
it('adds sphere impostor dots in row-major order when no LOD levels are configured', () => {
|
||||
const volume = createTestVolume([2, 2, 2], [
|
||||
1, 1,
|
||||
1, 1,
|
||||
1, 1,
|
||||
1, 1,
|
||||
]);
|
||||
const spheres = createVolumeSphereImpostor(undefined as any, volume, 0, undefined as any, {
|
||||
isoValue: Volume.IsoValue.absolute(0.5),
|
||||
perturbPositions: false,
|
||||
lodLevels: [],
|
||||
} as any);
|
||||
|
||||
expect(Array.from(spheres.groupBuffer.ref.value)).toEqual([0, 1, 2, 3, 4, 5, 6, 7]);
|
||||
});
|
||||
});
|
||||
@@ -67,8 +67,7 @@ export function VolumeSphereImpostorVisual(materialId: number): VolumeVisual<Vol
|
||||
setUpdateState: (state: VisualUpdateState, newVolume: Volume, currentVolume: Volume, newProps: PD.Values<VolumeSphereParams>, currentProps: PD.Values<VolumeSphereParams>, newTheme: Theme, currentTheme: Theme) => {
|
||||
state.createGeometry = (
|
||||
!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, newVolume.grid.stats) ||
|
||||
newProps.perturbPositions !== currentProps.perturbPositions ||
|
||||
newProps.lodLevels.length > 0 && currentProps.lodLevels.length === 0
|
||||
newProps.perturbPositions !== currentProps.perturbPositions
|
||||
);
|
||||
},
|
||||
geometryUtils: Spheres.Utils,
|
||||
@@ -129,71 +128,38 @@ export function createVolumeSphereImpostor(ctx: VisualContext, volume: Volume, k
|
||||
|
||||
const p = Vec3();
|
||||
const [xn, yn, zn] = space.dimensions;
|
||||
|
||||
const count = Math.ceil((xn * yn * zn) / 10);
|
||||
const builder = SpheresBuilder.create(count, Math.ceil(count / 2), spheres);
|
||||
|
||||
const invert = isoVal < 0;
|
||||
|
||||
// Precompute basis vectors and largest cell axis length
|
||||
const basis = props.perturbPositions ? getBasis(gridToCartn) : undefined;
|
||||
|
||||
const count = Math.ceil((xn * yn * zn) / 10);
|
||||
const builder = SpheresBuilder.create(count, Math.ceil(count / 2), spheres);
|
||||
for (let z = 0; z < zn; ++z) {
|
||||
for (let y = 0; y < yn; ++y) {
|
||||
for (let x = 0; x < xn; ++x) {
|
||||
const value = space.get(data, x, y, z);
|
||||
if (!invert && value < isoVal || invert && value > isoVal) continue;
|
||||
|
||||
const add = (x: number, y: number, z: number) => {
|
||||
const value = space.get(data, x, y, z);
|
||||
if (!invert && value < isoVal || invert && value > isoVal) return;
|
||||
|
||||
const cellIdx = space.dataOffset(x, y, z);
|
||||
Vec3.set(p, x, y, z);
|
||||
Vec3.transformMat4(p, p, gridToCartn);
|
||||
if (basis) {
|
||||
Vec3.add(p, p, getRandomOffsetFromBasis(basis));
|
||||
}
|
||||
builder.add(p[0], p[1], p[2], cellIdx);
|
||||
};
|
||||
|
||||
// Morton ordering keeps stride-based LOD sampling spatially balanced.
|
||||
// Only worthwhile when LOD levels are configured; otherwise use the
|
||||
// direct row-major path to avoid the extra allocations and sort.
|
||||
const useMortonOrder = props.lodLevels.length > 0;
|
||||
|
||||
if (useMortonOrder) {
|
||||
// Recursive octree traversal over the bounding power-of-two cube,
|
||||
// visiting children in Morton order (octant bit2=x, bit1=y, bit0=z).
|
||||
// Octants whose origin already exceeds the grid extent are pruned,
|
||||
// so out-of-range subtrees of non-cube grids cost ~O(log) per skip.
|
||||
let size = 1;
|
||||
while (size < xn || size < yn || size < zn) size <<= 1;
|
||||
|
||||
const visit = (x0: number, y0: number, z0: number, s: number): void => {
|
||||
if (x0 >= xn || y0 >= yn || z0 >= zn) return;
|
||||
|
||||
if (s === 1) {
|
||||
add(x0, y0, z0);
|
||||
return;
|
||||
}
|
||||
const h = s >> 1;
|
||||
visit(x0, y0, z0, h);
|
||||
visit(x0, y0, z0 + h, h);
|
||||
visit(x0, y0 + h, z0, h);
|
||||
visit(x0, y0 + h, z0 + h, h);
|
||||
visit(x0 + h, y0, z0, h);
|
||||
visit(x0 + h, y0, z0 + h, h);
|
||||
visit(x0 + h, y0 + h, z0, h);
|
||||
visit(x0 + h, y0 + h, z0 + h, h);
|
||||
};
|
||||
|
||||
visit(0, 0, 0, size);
|
||||
} else {
|
||||
for (let z = 0; z < zn; ++z) {
|
||||
for (let y = 0; y < yn; ++y) {
|
||||
for (let x = 0; x < xn; ++x) {
|
||||
add(x, y, z);
|
||||
const cellIdx = space.dataOffset(x, y, z);
|
||||
if (basis) {
|
||||
Vec3.set(p, x, y, z);
|
||||
Vec3.transformMat4(p, p, gridToCartn);
|
||||
const offset = getRandomOffsetFromBasis(basis);
|
||||
Vec3.add(p, p, offset);
|
||||
} else {
|
||||
Vec3.set(p, x, y, z);
|
||||
Vec3.transformMat4(p, p, gridToCartn);
|
||||
}
|
||||
builder.add(p[0], p[1], p[2], cellIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const s = builder.getSpheres();
|
||||
s.setBoundingSphere(Grid.getIsosurfaceBoundingSphere(volume.grid, Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue));
|
||||
s.setBoundingSphere(Volume.Isosurface.getBoundingSphere(volume, props.isoValue));
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -243,7 +209,7 @@ export function createVolumeSphereMesh(ctx: VisualContext, volume: Volume, key:
|
||||
}
|
||||
|
||||
const m = MeshBuilder.getMesh(builderState);
|
||||
m.setBoundingSphere(Grid.getIsosurfaceBoundingSphere(volume.grid, Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue));
|
||||
m.setBoundingSphere(Volume.Isosurface.getBoundingSphere(volume, props.isoValue));
|
||||
return m;
|
||||
}
|
||||
|
||||
@@ -311,7 +277,7 @@ export function createVolumePoint(ctx: VisualContext, volume: Volume, key: numbe
|
||||
}
|
||||
|
||||
const pt = builder.getPoints();
|
||||
pt.setBoundingSphere(Grid.getIsosurfaceBoundingSphere(volume.grid, Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue));
|
||||
pt.setBoundingSphere(Volume.Isosurface.getBoundingSphere(volume, props.isoValue));
|
||||
return pt;
|
||||
}
|
||||
|
||||
@@ -354,7 +320,6 @@ const DotVisuals = {
|
||||
export const DotParams = {
|
||||
...VolumeSphereParams,
|
||||
...VolumePointParams,
|
||||
sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
|
||||
visuals: PD.MultiSelect(['sphere'], PD.objectToOptions(DotVisuals)),
|
||||
bumpFrequency: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
};
|
||||
@@ -381,4 +346,4 @@ export const DotRepresentationProvider = VolumeRepresentationProvider({
|
||||
defaultSizeTheme: { name: 'uniform' },
|
||||
locationKinds: ['cell-location', 'position-location'],
|
||||
isApplicable: (volume: Volume) => !Volume.isEmpty(volume) && !Volume.Segmentation.get(volume)
|
||||
});
|
||||
});
|
||||
@@ -136,7 +136,7 @@ export async function createVolumeIsosurfaceMesh(ctx: VisualContext, volume: Vol
|
||||
ValueCell.updateIfChanged(surface.varyingGroup, true);
|
||||
}
|
||||
|
||||
surface.setBoundingSphere(Grid.getIsosurfaceBoundingSphere(volume.grid, Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue));
|
||||
surface.setBoundingSphere(Volume.Isosurface.getBoundingSphere(volume, props.isoValue));
|
||||
|
||||
return surface;
|
||||
}
|
||||
@@ -318,7 +318,7 @@ export async function createVolumeIsosurfaceWireframe(ctx: VisualContext, volume
|
||||
const transform = Grid.getGridToCartesianTransform(volume.grid);
|
||||
Lines.transform(wireframe, transform);
|
||||
|
||||
wireframe.setBoundingSphere(Grid.getIsosurfaceBoundingSphere(volume.grid, Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue));
|
||||
wireframe.setBoundingSphere(Volume.Isosurface.getBoundingSphere(volume, props.isoValue));
|
||||
|
||||
return wireframe;
|
||||
}
|
||||
|
||||
@@ -306,7 +306,7 @@ function getSampledImage(volume: Volume, theme: Theme, info: SamplingInfo, isoVa
|
||||
const isoLevel = clamp(normalize(Volume.IsoValue.toAbsolute(isoValue, stats).absoluteValue, min, max), 0, 1);
|
||||
|
||||
const im = Image.create(imageTexture, corners, groupTexture, valueTexture, trim, isoLevel, image);
|
||||
im.setBoundingSphere(Volume.isPeriodic(volume) ? Volume.getBoundingSphere(volume, Interval.ofLength(volume.instances.length as Volume.InstanceIndex)) : Grid.getBoundingSphere(volume.grid));
|
||||
im.setBoundingSphere(Volume.isPeriodic(volume) ? Volume.getBoundingSphere(volume) : Grid.getBoundingSphere(volume.grid));
|
||||
im.meta.mapping = mapping;
|
||||
|
||||
return im;
|
||||
@@ -480,7 +480,7 @@ async function createGridImage(ctx: VisualContext, volume: Volume, key: number,
|
||||
const isoLevel = clamp(normalize(Volume.IsoValue.toAbsolute(isoValue, stats).absoluteValue, min, max), 0, 1);
|
||||
|
||||
const im = Image.create(imageTexture, corners, groupTexture, valueTexture, trim, isoLevel, image);
|
||||
im.setBoundingSphere(Volume.isPeriodic(volume) ? Volume.getBoundingSphere(volume, Interval.ofLength(volume.instances.length as Volume.InstanceIndex)) : Grid.getBoundingSphere(volume.grid));
|
||||
im.setBoundingSphere(Volume.isPeriodic(volume) ? Volume.getBoundingSphere(volume) : Grid.getBoundingSphere(volume.grid));
|
||||
im.meta.mapping = mapping;
|
||||
|
||||
return im;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author ReliaSolve <russ@reliasolve.com>
|
||||
*
|
||||
*
|
||||
* Adapted from kin-parser.ts file from the NGL project:
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* Adapted from hsl.ts in this same directory:
|
||||
@@ -27,48 +27,48 @@ function Hsv() {
|
||||
|
||||
namespace Hsv {
|
||||
export function zero(): Hsv {
|
||||
const out = [0.0, 0.0, 0.0];
|
||||
out[0] = 0;
|
||||
const out = [0.0, 0.0, 0.0]
|
||||
out[0] = 0
|
||||
return out as Hsv;
|
||||
}
|
||||
|
||||
/** Copy values from an array-like 3-tuple into `out`. */
|
||||
export function fromArray(arr: ArrayLike<number>): Hsv {
|
||||
const out = Hsv.zero();
|
||||
out[0] = arr[0] ?? 0;
|
||||
out[1] = arr[1] ?? 0;
|
||||
out[2] = arr[2] ?? 0;
|
||||
return out;
|
||||
out[0] = arr[0] ?? 0
|
||||
out[1] = arr[1] ?? 0
|
||||
out[2] = arr[2] ?? 0
|
||||
return out
|
||||
}
|
||||
|
||||
const _rgb = Rgb();
|
||||
export function toColor(hsv: Hsv): Color {
|
||||
toRgb(_rgb, hsv);
|
||||
return Rgb.toColor(_rgb);
|
||||
return Rgb.toColor(_rgb)
|
||||
}
|
||||
|
||||
export function toRgb(out: Rgb, hsv: Hsv) {
|
||||
let [h, s, v] = hsv;
|
||||
h /= 360;
|
||||
s /= 100;
|
||||
v /= 100;
|
||||
let r = 0, g = 0, b = 0;
|
||||
const i = Math.floor(h * 6);
|
||||
const f = h * 6 - i;
|
||||
const p = v * (1 - s);
|
||||
const q = v * (1 - f * s);
|
||||
const t = v * (1 - (1 - f) * s);
|
||||
h /= 360
|
||||
s /= 100
|
||||
v /= 100
|
||||
let r = 0, g = 0, b = 0
|
||||
const i = Math.floor(h * 6)
|
||||
const f = h * 6 - i
|
||||
const p = v * (1 - s)
|
||||
const q = v * (1 - f * s)
|
||||
const t = v * (1 - (1 - f) * s)
|
||||
switch (i % 6) {
|
||||
case 0: r = v; g = t; b = p; break;
|
||||
case 1: r = q; g = v; b = p; break;
|
||||
case 2: r = p; g = v; b = t; break;
|
||||
case 3: r = p; g = q; b = v; break;
|
||||
case 4: r = t; g = p; b = v; break;
|
||||
case 5: r = v; g = p; b = q; break;
|
||||
case 0: r = v; g = t; b = p; break
|
||||
case 1: r = q; g = v; b = p; break
|
||||
case 2: r = p; g = v; b = t; break
|
||||
case 3: r = p; g = q; b = v; break
|
||||
case 4: r = t; g = p; b = v; break
|
||||
case 5: r = v; g = p; b = q; break
|
||||
}
|
||||
out[0] = r;
|
||||
out[1] = g;
|
||||
out[2] = b;
|
||||
return out;
|
||||
out[0] = r
|
||||
out[1] = g
|
||||
out[2] = b
|
||||
return out
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
*/
|
||||
|
||||
export { PixelData };
|
||||
@@ -38,14 +37,12 @@ namespace PixelData {
|
||||
/** to undo pre-multiplied alpha */
|
||||
export function divideByAlpha(pixelData: PixelData): PixelData {
|
||||
const { array } = pixelData;
|
||||
// clamp: emissive, bloom and antialiasing can lift premul RGB above alpha; without it Uint8Array silently wraps.
|
||||
const max = (array instanceof Uint8Array) ? 255 : 1;
|
||||
const factor = (array instanceof Uint8Array) ? 255 : 1;
|
||||
for (let i = 0, il = array.length; i < il; i += 4) {
|
||||
const a = array[i + 3] / max;
|
||||
if (a === 0) continue;
|
||||
array[i] = Math.min(max, array[i] / a);
|
||||
array[i + 1] = Math.min(max, array[i + 1] / a);
|
||||
array[i + 2] = Math.min(max, array[i + 2] / a);
|
||||
const a = array[i + 3] / factor;
|
||||
array[i] /= a;
|
||||
array[i + 1] /= a;
|
||||
array[i + 2] /= a;
|
||||
}
|
||||
return pixelData;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
# 0.9.13
|
||||
* /surroundingLigands: honor `omit_water=true|false` for REST GET requests (boolean parser previously coerced both to `false`)
|
||||
* /surroundingLigands: stop leaking the asymmetric unit's water chain into the result when `omit_water=true` (water residues pulled in via struct_conn covale/metalc edges no longer match every other water in the chain)
|
||||
|
||||
# 0.9.12
|
||||
* add `health-check` endpoint + `healthCheckPath` config prop to report service health
|
||||
|
||||
|
||||
@@ -295,7 +295,7 @@ function _normalizeQueryParams(params: { [p: string]: string }, paramList: Query
|
||||
case QueryParamType.String: el = value; break;
|
||||
case QueryParamType.Integer: el = parseInt(value); break;
|
||||
case QueryParamType.Float: el = parseFloat(value); break;
|
||||
case QueryParamType.Boolean: el = isTrue(value); break;
|
||||
case QueryParamType.Boolean: el = Boolean(+value); break;
|
||||
}
|
||||
|
||||
if (p.validation) p.validation(el);
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
export const VERSION = '0.9.13';
|
||||
export const VERSION = '0.9.12';
|
||||
Reference in New Issue
Block a user