Compare commits

..

243 Commits

Author SHA1 Message Date
Alexander Rose
a3b54ff88c lint/format 2026-05-09 22:08:08 -07:00
Alexander Rose
3e7614d75c move spec 2026-05-09 17:37:47 -07:00
Alexander Rose
a01e8f26bd changelog 2026-05-09 17:33:31 -07:00
Alexander Rose
351faf3c45 Merge branch 'master' of https://github.com/molstar/molstar into pr/russell-taylor/1806 2026-05-09 17:32:53 -07:00
Russ Taylor
f500372c16 Re-styling the Kinemage extension right-hand UI to better match MolStar style 2026-05-05 08:50:17 -04:00
Russ Taylor
2714d32e15 Implementing @pointmaster behavior properly. 2026-05-04 12:20:18 -04:00
Russ Taylor
2d400d9166 Updating README 2026-04-27 10:36:01 -04:00
Russ Taylor
ebceecb3e6 Set background color to black when selecting a Kinemage view 2026-04-27 10:33:01 -04:00
Russ Taylor
a87f92bf7d Adding * in front of animation groups 2026-04-27 10:27:20 -04:00
Russ Taylor
4033bc93c2 Adding subgroup visibility controls under groups when appropriate 2026-04-27 10:23:49 -04:00
Russ Taylor
6c4ba7af61 Removing unused activeKinemage index 2026-04-27 10:13:49 -04:00
Russ Taylor
bc9584e49b Removing global state and using Transforms instead. Unregistering right-hand-side GUI objects when their associated State Tree objects are deleted. 2026-04-27 10:00:19 -04:00
Russ Taylor
550d898c4f Renaming file and removing commented-out code. 2026-04-20 14:44:49 -04:00
Russ Taylor
5e16c340dc Converting Kinemage parser to using Color rather than number[] and moving HSV conversion into standard location 2026-04-20 14:34:05 -04:00
Russ Taylor
bcb18a8faf Reverting another change no longer needed. 2026-04-20 11:32:31 -04:00
Russ Taylor
cd7d8f704e Reverting changes made to get things to compile on an earlier master branch 2026-04-20 11:25:10 -04:00
Russ Taylor
0d197b2dc5 Merge branch 'kinemage-rebase' of https://github.com/ReliaSolve/molstar into kinemage-rebase 2026-04-20 09:38:26 -04:00
Russ Taylor
b3ce268f0e Reverting changes to central files that I thought I had to make to get the code to compile on a previous master checkout. The repository compiles without them now. 2026-04-20 09:37:51 -04:00
Russ Taylor
2da02daadc Comment change 2026-04-20 09:29:06 -04:00
Russ Taylor
999e5a47af Comment and README changes 2026-04-20 09:29:06 -04:00
Russ Taylor
ff3fad0789 Allow loading of multiple kinemages, seeing the controls for all of them. 2026-04-20 09:29:06 -04:00
Russ Taylor
5af6265c07 Putting back animation controls and maintaining views across visibility changes 2026-04-20 09:29:06 -04:00
Russ Taylor
4ad7a96191 Visibility of Kinemage controls now working and they are showing up in the right-hand control panel. 2026-04-20 09:29:06 -04:00
Russ Taylor
71215d183d Continuing to implement controls on the right. 2026-04-20 09:29:06 -04:00
Russ Taylor
e864f13a66 Starting down the path of moving the Kinemage GUI controls to the right-side panel. Puts the placeholder there but now shows only part of the geometry and does not see any Kinemage data. 2026-04-20 09:29:06 -04:00
Russ Taylor
59298be573 Moving kin.ts into extensions/kinemage 2026-04-20 09:29:06 -04:00
Russ Taylor
2a57867167 Moving reader code for kinemage into its extensions directory 2026-04-20 09:29:06 -04:00
Russ Taylor
a817a70b46 Revering whitespace edits. 2026-04-20 09:29:06 -04:00
Russ Taylor
5afb7981ae Removing grammar fix and carriage return at end of file. 2026-04-20 09:29:06 -04:00
Russ Taylor
2308dfd22e Removing extra line added to file. 2026-04-20 09:29:06 -04:00
Russ Taylor
f0de2cea2c Fixing author tags in documents 2026-04-20 09:29:06 -04:00
Russ Taylor
8f9c687935 Removing obsolete entry 2026-04-20 09:29:06 -04:00
Russ Taylor
1f56405d79 Updating contributer documentation 2026-04-20 09:29:06 -04:00
Russ Taylor
37af8c66a1 This version requires us not to flip the winding numbers of every other triangle so that the colors match. 2026-04-20 09:29:06 -04:00
Russ Taylor
a1fefa2efa Adding KinemageExtension to viewer app 2026-04-20 09:29:06 -04:00
Russ Taylor
958f3011e4 Fixing assignments to handle strings or string arrays to allow the code to compile 2026-04-20 09:29:03 -04:00
Russ Taylor
8d48fa67ae Removing initial plugin-based Kinemage reader stubs, leaving the extension that handles both File/Open and drag-and-drop 2026-04-20 09:27:41 -04:00
Russ Taylor
87158db7c0 Removing obsolete @todo comments 2026-04-20 09:27:41 -04:00
Russ Taylor
bac65cc71e Animation toggles visibility checkboxes on the groups as it runs. 2026-04-20 09:27:41 -04:00
Russ Taylor
3ece0c74c6 Strating down the path of handling GUI updates with animation 2026-04-20 09:27:41 -04:00
Russ Taylor
89fbb690fe Adding animate and 2animate buttons that do not yet adjust the GUI state to track the changes 2026-04-20 09:27:41 -04:00
Russ Taylor
918d02fec4 Make the Transforms associated with the geometry into ghosts so they don't show up in the GUI 2026-04-20 09:27:41 -04:00
Russ Taylor
8af3a240b5 Destroy old objects when we change visibility rather than just hiding them 2026-04-20 09:27:41 -04:00
Russ Taylor
a80284ac02 Fixing parsing of nobutton tag on list 2026-04-20 09:27:41 -04:00
Russ Taylor
1b48f4c32a Removing spurious declaration 2026-04-20 09:27:41 -04:00
Russ Taylor
1055eab4c5 Removing unused parameter 2026-04-20 09:27:41 -04:00
Russ Taylor
d4445cef5c Turning all but the first group that is in animate off 2026-04-20 09:27:41 -04:00
Russ Taylor
063d327a5f Updating README 2026-04-20 09:27:41 -04:00
Russ Taylor
34f409e683 Handling 'nobutton' keyword when parsing and also fixing the display of GUI elements 2026-04-20 09:27:41 -04:00
Russ Taylor
8415ed1b92 Orders GUI elements so that subgroups after their group 2026-04-20 09:27:41 -04:00
Russ Taylor
c92147289e Adding subgroup visibility controls 2026-04-20 09:27:41 -04:00
Russ Taylor
5b16213cd2 Cleaning up the visibility calculations for both masters and groups 2026-04-20 09:27:41 -04:00
Russ Taylor
bbd36e1838 Adding group visibility controls. 2026-04-20 09:27:41 -04:00
Russ Taylor
1a0b30d6eb Updating Kinemage README with new capabilities 2026-04-20 09:27:41 -04:00
Russ Taylor
e309e8917a Split each vector in half, label and color each half by the nearest endpoint. This makes the pop-up labels match what is expected 2026-04-20 09:27:41 -04:00
Russ Taylor
14135b8386 Ghosting the visibility controls for shapes in kinemages because they will be controlled by the masters and groups 2026-04-20 09:27:41 -04:00
Russ Taylor
3230a6a7dc Don't repeat kinemage construction when a later file is loaded 2026-04-20 09:27:41 -04:00
Russ Taylor
34ebc5ab7a Keep the viewpoint from changing when we make masters visible and invisible 2026-04-20 09:27:41 -04:00
Russ Taylor
d8b62c5cbb Removing obsolete view code 2026-04-20 09:27:41 -04:00
Russ Taylor
358ef44780 Master visibility now working, though it causes view recentering. Removed spurious calls from view adjustment but still happening 2026-04-20 09:27:41 -04:00
Russ Taylor
fb2f79a395 Factoring out shape creation function so we can call it again later. Keeping track of kinData 2026-04-20 09:27:41 -04:00
Russ Taylor
e3a95e0a08 Keeping track of the shapes that are created for a kinemage 2026-04-20 09:27:41 -04:00
Russ Taylor
a1b09ccc1c Adding group and subgroup visibility calculations to kinemage files 2026-04-20 09:27:41 -04:00
Russ Taylor
26b31b3fcc Adding off entry for groups and subgroups that defaults to false 2026-04-20 09:27:41 -04:00
Russ Taylor
3027418d31 Control the geometry generation in kinemages based on the visibility of masters for each list. This is not yet tied into changes caused by the visibility buttons, but it now respects the initial states of the masters in the kinemage file. 2026-04-20 09:27:41 -04:00
Russ Taylor
5a69fb691d Updating default visibility when parsing kinemage files. Adding master controls whose visibility icons toggle the state. This does not yet change the visibility of objects in the scene 2026-04-20 09:27:41 -04:00
Russ Taylor
cced98c93f Separating the parsing and geometry generation for kinemages 2026-04-20 09:27:41 -04:00
Russ Taylor
6c299161fe Removing obsolete comment 2026-04-20 09:27:41 -04:00
Russ Taylor
10575ac361 Updating README 2026-04-20 09:27:41 -04:00
Russ Taylor
d715330d8e Tweak 2026-04-20 09:27:41 -04:00
Russ Taylor
fdba049982 Changing the name of the view selection GUI elements to match the view that they provide. 2026-04-20 09:27:41 -04:00
Russ Taylor
270d7386b2 Transposing the orientation matrix to match Mol* orientation 2026-04-20 09:27:41 -04:00
Russ Taylor
a28c2f0995 Adding GUI elements to select Views when they are present in the Kinemage file. 2026-04-20 09:27:41 -04:00
Russ Taylor
196e17ff0d Constructing Camera.Snapshot objects for each Kinemage View. 2026-04-20 09:27:41 -04:00
Russ Taylor
c7efac0a78 Adding parsing of view parameters from Kinemage 2026-04-20 09:27:41 -04:00
Russ Taylor
75eb04070c Updating README 2026-04-20 09:27:41 -04:00
Russ Taylor
d6c9ae1fbe Naming the GUI elements after the PDB file if it is specified in the Kinemage file 2026-04-20 09:27:41 -04:00
Russ Taylor
24a6403025 Enabling specifying the name of a geometry type loaded by a Kinemage. Not adding entries for object types that are empty lists 2026-04-20 09:27:41 -04:00
Russ Taylor
842e5d890e Simplifying function 2026-04-20 09:27:41 -04:00
Russ Taylor
b34d1cca00 Removing unused objects 2026-04-20 09:27:41 -04:00
Russ Taylor
af4dc090c4 Removing unused objects left over from original code copied from 2026-04-20 09:27:41 -04:00
Russ Taylor
1fa090d162 Removing usused Preset 2026-04-20 09:27:41 -04:00
Russ Taylor
4d5b749e3e Cleaning up unused objects 2026-04-20 09:27:41 -04:00
Russ Taylor
6a736eb89f Updating comments 2026-04-20 09:27:41 -04:00
Russ Taylor
cfead0481f Simplifying function 2026-04-20 09:27:41 -04:00
Russ Taylor
fbbd7e623e Removing de-duplication code 2026-04-20 09:27:41 -04:00
Russ Taylor
f16707b849 Hack of commenting out the visuals to make it only parse once 2026-04-20 09:27:41 -04:00
Russ Taylor
76b0b23c07 Wraps the text in a file when loading, but this causes it to be parsed twice. 2026-04-20 09:27:41 -04:00
Russ Taylor
d4a2bd7cba Starting to implement standard file loading for .kin files 2026-04-20 09:27:41 -04:00
Russ Taylor
c572feb1d2 Factoring out file loading from drag and drop handler 2026-04-20 09:27:41 -04:00
Russ Taylor
121f8eab3e Updating README 2026-04-20 09:27:41 -04:00
Russ Taylor
8c49b82c3d Updating README 2026-04-20 09:27:41 -04:00
Russ Taylor
ac7faf8524 Adding README.md for Kinemage extension 2026-04-20 09:27:41 -04:00
Russ Taylor
5d6d91a331 Making Kinemage ribbons have the same normal for every pair of triangles 2026-04-20 09:27:41 -04:00
Russ Taylor
d476db556d Adding labels to elements loaded from Kinemage 2026-04-20 09:27:41 -04:00
Russ Taylor
354d092834 Adding per-sphere coloring 2026-04-20 09:27:41 -04:00
Russ Taylor
5cde26a8e2 Adding per-dot coloring 2026-04-20 09:27:41 -04:00
Russ Taylor
b905178395 Fixing per-group coloring on meshes and cleaning up 2026-04-20 09:27:41 -04:00
Russ Taylor
543d014d0d Enabling support for coloring of ribbons, including rendering both sides with the same color. 2026-04-20 09:27:41 -04:00
Russ Taylor
a17afa59b3 Adding line coloring. 2026-04-20 09:27:41 -04:00
Russ Taylor
bd6be354d5 Handling @colorset lines in Kinemage. Reporting when we have an unrecognized list element. 2026-04-20 09:27:41 -04:00
Russ Taylor
2058d605c7 Setting Kinemage line radius as half the width, clamped to a minimum of 1.0 2026-04-20 09:27:41 -04:00
Russ Taylor
ba60188758 Fixing width code on vectors. Cleaning up color code 2026-04-20 09:27:40 -04:00
Russ Taylor
52f2ddf715 Adding control over line width. Allowing short forms of list names. Working on passing color through 2026-04-20 09:27:40 -04:00
Russ Taylor
aa20fffbfb Adding sphere generation for BallList in Kinemage files. Added reading of radius from list line to enable list-wide specification 2026-04-20 09:27:40 -04:00
Russ Taylor
b59d11c91a Fixing over-counting of points 2026-04-20 09:27:40 -04:00
Russ Taylor
8fdc29d048 Reducing the number of objects and commits 2026-04-20 09:27:40 -04:00
Russ Taylor
d52ea41051 Only reporting an opened file if we get a kinemage 2026-04-20 09:27:40 -04:00
Russ Taylor
852be261dd More work but less fragile 2026-04-20 09:27:40 -04:00
Russ Taylor
95e9a3012d Continued cleanup 2026-04-20 09:27:40 -04:00
Russ Taylor
dd0a45c154 Cleaning up 2026-04-20 09:27:40 -04:00
Russ Taylor
4692d63a2b More cleanup 2026-04-20 09:27:40 -04:00
Russ Taylor
0689ecabb6 Cleaning up nesting and variables 2026-04-20 09:27:40 -04:00
Russ Taylor
424f576e99 Also draws points for dotLists in Kinemage 2026-04-20 09:27:40 -04:00
Russ Taylor
4407994195 Kinemage drag-and-drop handler now shows both lines and ribbons 2026-04-20 09:27:40 -04:00
Russ Taylor
4c6331e72d Renaming the Kinemage shape provider pipeline to include the name lines so we can make separate ones for meshes and balls 2026-04-20 09:27:40 -04:00
Russ Taylor
825514dd10 First working Kinemage extension code that can draw lines from all drag-and-drop kinemages 2026-04-20 09:27:40 -04:00
Russ Taylor
8e2967b993 Changing name to kinemage 2026-04-20 09:27:40 -04:00
Russ Taylor
a46ba63e31 Fixing name on KinemageDataProvider 2026-04-20 09:27:40 -04:00
Russ Taylor
cf9fe99a81 Renaming for clarity 2026-04-20 09:27:40 -04:00
Russ Taylor
2909a209c3 Modifying the kinemage parser to be able to read more than one kinemage entry from the same file. Hooking in a drag and drop handler on .kin files that for now just stores things into a global variable. Adjusting the parsing of kinemage through the original plugin path to handle the new parsing. 2026-04-20 09:27:40 -04:00
Russ Taylor
1322882444 Duplicating ANVIL structure in a kinemage extension directory as a basis for a new Kinemage loading extension
Trying to stick with the master code, which has changed a lot.
2026-04-20 09:27:40 -04:00
Russ Taylor
119c548fa7 Initial implementation of converting vectors into lines. Still needs groups, colors, labels, etc. 2026-04-20 09:27:40 -04:00
Russ Taylor
539442f710 Initial construction of lines from vector lists. Still need to do multiple vector lists, colors, labels, and more. 2026-04-20 09:27:40 -04:00
Russ Taylor
8841f04af6 Removing copied PLY parsing and geometry generation code from KIN file reader. Passing the Kinemage data structure from the parser to the geometry generator. Stubs now in place for KIN with no geometry currently being generated. Also updated the kin.spec.ts file to check a Kinemage file. 2026-04-20 09:27:40 -04:00
Russ Taylor
8c2d3a577a Organizing and commenting list structures. Specifying types where known. 2026-04-20 09:27:40 -04:00
Russ Taylor
bed4b728d3 Moving interface definitions to scheme.ts for Kin file reader. 2026-04-20 09:27:40 -04:00
Russ Taylor
aa87acc0a7 Initial pull-in of NGL Kinemage parser code. It is called by parser.ts and counts of objects are printed. 2026-04-20 09:27:40 -04:00
Russ Taylor
7d1e2b44db Adding KIN loading to mesoscale app 2026-04-20 09:27:40 -04:00
Russ Taylor
c1c1badf62 Initial Kinemage commit that copies the PLY files and references to make it possible to load a PLY-formate file from a file with a KIN extension.
Overwriting package-lock.json
2026-04-20 09:27:40 -04:00
Russ Taylor
e270a83909 Comment change 2026-04-13 13:42:00 -04:00
Russ Taylor
a40b737c6f Comment and README changes 2026-04-13 13:40:52 -04:00
Russ Taylor
942533ed2b Allow loading of multiple kinemages, seeing the controls for all of them. 2026-04-13 13:28:24 -04:00
Russ Taylor
546f3cd3c5 Putting back animation controls and maintaining views across visibility changes 2026-04-13 13:23:02 -04:00
Russ Taylor
21597b1fdd Visibility of Kinemage controls now working and they are showing up in the right-hand control panel. 2026-04-13 12:29:27 -04:00
Russ Taylor
31d8568c1a Continuing to implement controls on the right. 2026-04-13 12:11:46 -04:00
Russ Taylor
3630cd14e8 Starting down the path of moving the Kinemage GUI controls to the right-side panel. Puts the placeholder there but now shows only part of the geometry and does not see any Kinemage data. 2026-04-13 11:18:07 -04:00
Russ Taylor
4f083f10e6 Moving kin.ts into extensions/kinemage 2026-04-13 09:02:59 -04:00
Russ Taylor
371ef984c0 Moving reader code for kinemage into its extensions directory 2026-04-13 08:58:27 -04:00
Russ Taylor
e2db1257cd Revering whitespace edits. 2026-04-13 08:36:49 -04:00
Russ Taylor
c812e72a1a Removing grammar fix and carriage return at end of file. 2026-04-13 08:32:35 -04:00
Russ Taylor
ef9b89820d Removing extra line added to file. 2026-04-13 08:30:47 -04:00
Russ Taylor
5fd453c77a Fixing author tags in documents 2026-04-03 17:28:29 -04:00
Russ Taylor
7f2b10674e Removing obsolete entry 2026-04-03 17:26:50 -04:00
Russ Taylor
238e5e0b88 Updating contributer documentation 2026-04-03 16:58:28 -04:00
Russ Taylor
1f26b5c339 This version requires us not to flip the winding numbers of every other triangle so that the colors match. 2026-04-03 16:51:55 -04:00
Russ Taylor
eac478e7cb Adding KinemageExtension to viewer app 2026-04-03 16:45:26 -04:00
Russ Taylor
d0c59fdc92 Fixing assignments to handle strings or string arrays to allow the code to compile 2026-04-03 16:26:28 -04:00
Russ Taylor
7e61bcad32 Overwriting package-lock.json based on new build 2026-04-03 13:56:49 -04:00
Russ Taylor
7e98870dce Removing initial plugin-based Kinemage reader stubs, leaving the extension that handles both File/Open and drag-and-drop 2026-04-03 13:30:45 -04:00
Russ Taylor
405dc0d90a Removing obsolete @todo comments 2026-04-03 13:30:45 -04:00
Russ Taylor
7a362c816e Animation toggles visibility checkboxes on the groups as it runs. 2026-04-03 13:30:45 -04:00
Russ Taylor
7e1396b74c Strating down the path of handling GUI updates with animation 2026-04-03 13:30:45 -04:00
Russ Taylor
68ad1ec065 Adding animate and 2animate buttons that do not yet adjust the GUI state to track the changes 2026-04-03 13:30:45 -04:00
Russ Taylor
430f8da44e Make the Transforms associated with the geometry into ghosts so they don't show up in the GUI 2026-04-03 13:30:45 -04:00
Russ Taylor
68866cd2de Destroy old objects when we change visibility rather than just hiding them 2026-04-03 13:30:45 -04:00
Russ Taylor
05888bec50 Fixing parsing of nobutton tag on list 2026-04-03 13:30:45 -04:00
Russ Taylor
65e1cb4a5d Removing spurious declaration 2026-04-03 13:30:45 -04:00
Russ Taylor
50f571b0d3 Removing unused parameter 2026-04-03 13:30:45 -04:00
Russ Taylor
d86b31edf8 Turning all but the first group that is in animate off 2026-04-03 13:30:45 -04:00
Russ Taylor
ec107352b4 Updating README 2026-04-03 13:30:45 -04:00
Russ Taylor
1d42d5a2d6 Handling 'nobutton' keyword when parsing and also fixing the display of GUI elements 2026-04-03 13:30:45 -04:00
Russ Taylor
02d1dcb9d9 Orders GUI elements so that subgroups after their group 2026-04-03 13:30:45 -04:00
Russ Taylor
d86c3621b7 Adding subgroup visibility controls 2026-04-03 13:30:45 -04:00
Russ Taylor
f2724491c2 Cleaning up the visibility calculations for both masters and groups 2026-04-03 13:30:45 -04:00
Russ Taylor
f4c84a6930 Adding group visibility controls. 2026-04-03 13:30:45 -04:00
Russ Taylor
0eb9b286b4 Updating Kinemage README with new capabilities 2026-04-03 13:30:45 -04:00
Russ Taylor
da006391da Split each vector in half, label and color each half by the nearest endpoint. This makes the pop-up labels match what is expected 2026-04-03 13:30:45 -04:00
Russ Taylor
130e33f8c3 Ghosting the visibility controls for shapes in kinemages because they will be controlled by the masters and groups 2026-04-03 13:30:45 -04:00
Russ Taylor
109e528d1c Don't repeat kinemage construction when a later file is loaded 2026-04-03 13:30:45 -04:00
Russ Taylor
5df69cd84a Keep the viewpoint from changing when we make masters visible and invisible 2026-04-03 13:30:45 -04:00
Russ Taylor
973afa2237 Removing obsolete view code 2026-04-03 13:30:45 -04:00
Russ Taylor
0088d3e1bf Master visibility now working, though it causes view recentering. Removed spurious calls from view adjustment but still happening 2026-04-03 13:30:45 -04:00
Russ Taylor
26e6a11fa8 Factoring out shape creation function so we can call it again later. Keeping track of kinData 2026-04-03 13:30:45 -04:00
Russ Taylor
056e2c5182 Keeping track of the shapes that are created for a kinemage 2026-04-03 13:30:45 -04:00
Russ Taylor
0e7cde24bc Adding group and subgroup visibility calculations to kinemage files 2026-04-03 13:30:45 -04:00
Russ Taylor
36ce262970 Adding off entry for groups and subgroups that defaults to false 2026-04-03 13:30:45 -04:00
Russ Taylor
289c8181c8 Control the geometry generation in kinemages based on the visibility of masters for each list. This is not yet tied into changes caused by the visibility buttons, but it now respects the initial states of the masters in the kinemage file. 2026-04-03 13:30:45 -04:00
Russ Taylor
eb0fd490d4 Updating default visibility when parsing kinemage files. Adding master controls whose visibility icons toggle the state. This does not yet change the visibility of objects in the scene 2026-04-03 13:30:45 -04:00
Russ Taylor
70c073c43c Separating the parsing and geometry generation for kinemages 2026-04-03 13:30:45 -04:00
Russ Taylor
0565df4df9 Removing obsolete comment 2026-04-03 13:30:45 -04:00
Russ Taylor
6dd425cb55 Updating README 2026-04-03 13:30:45 -04:00
Russ Taylor
c0f994a506 Tweak 2026-04-03 13:30:45 -04:00
Russ Taylor
63705ed158 Changing the name of the view selection GUI elements to match the view that they provide. 2026-04-03 13:30:45 -04:00
Russ Taylor
fda9069f17 Transposing the orientation matrix to match Mol* orientation 2026-04-03 13:30:45 -04:00
Russ Taylor
66bffd8403 Adding GUI elements to select Views when they are present in the Kinemage file. 2026-04-03 13:30:45 -04:00
Russ Taylor
4a7d83c85b Constructing Camera.Snapshot objects for each Kinemage View. 2026-04-03 13:30:45 -04:00
Russ Taylor
fef649ce09 Adding parsing of view parameters from Kinemage 2026-04-03 13:30:45 -04:00
Russ Taylor
794f81bb8e Updating README 2026-04-03 13:30:45 -04:00
Russ Taylor
bc5648620d Naming the GUI elements after the PDB file if it is specified in the Kinemage file 2026-04-03 13:30:45 -04:00
Russ Taylor
07897f57f3 Enabling specifying the name of a geometry type loaded by a Kinemage. Not adding entries for object types that are empty lists 2026-04-03 13:30:45 -04:00
Russ Taylor
77f756dfe0 Simplifying function 2026-04-03 13:30:44 -04:00
Russ Taylor
15eef7b688 Removing unused objects 2026-04-03 13:30:44 -04:00
Russ Taylor
5d0ba7504b Removing unused objects left over from original code copied from 2026-04-03 13:30:44 -04:00
Russ Taylor
8d59b5b814 Removing usused Preset 2026-04-03 13:30:44 -04:00
Russ Taylor
86871124d5 Cleaning up unused objects 2026-04-03 13:30:44 -04:00
Russ Taylor
1a328d98b6 Updating comments 2026-04-03 13:30:44 -04:00
Russ Taylor
fcbc3ab3d0 Simplifying function 2026-04-03 13:30:44 -04:00
Russ Taylor
f36093dad9 Removing de-duplication code 2026-04-03 13:30:44 -04:00
Russ Taylor
d4c2bb85cb Hack of commenting out the visuals to make it only parse once 2026-04-03 13:30:44 -04:00
Russ Taylor
d53c1e8e65 Wraps the text in a file when loading, but this causes it to be parsed twice. 2026-04-03 13:30:44 -04:00
Russ Taylor
f189d0bdab Starting to implement standard file loading for .kin files 2026-04-03 13:30:44 -04:00
Russ Taylor
395eddd927 Factoring out file loading from drag and drop handler 2026-04-03 13:30:44 -04:00
Russ Taylor
eb1d48a73c Updating README 2026-04-03 13:30:44 -04:00
Russ Taylor
38b0bb8d7d Updating README 2026-04-03 13:30:44 -04:00
Russ Taylor
0daffa6b57 Adding README.md for Kinemage extension 2026-04-03 13:30:44 -04:00
Russ Taylor
e1d5d369f1 Making Kinemage ribbons have the same normal for every pair of triangles 2026-04-03 13:30:44 -04:00
Russ Taylor
6b88acd2bc Adding labels to elements loaded from Kinemage 2026-04-03 13:30:44 -04:00
Russ Taylor
6384eac7e7 Adding per-sphere coloring 2026-04-03 13:30:44 -04:00
Russ Taylor
fd409ce27f Adding per-dot coloring 2026-04-03 13:30:44 -04:00
Russ Taylor
c21c9f5160 Fixing per-group coloring on meshes and cleaning up 2026-04-03 13:30:44 -04:00
Russ Taylor
71d00a22dd Enabling support for coloring of ribbons, including rendering both sides with the same color. 2026-04-03 13:30:44 -04:00
Russ Taylor
5554697028 Adding line coloring. 2026-04-03 13:30:44 -04:00
Russ Taylor
709ac8430a Handling @colorset lines in Kinemage. Reporting when we have an unrecognized list element. 2026-04-03 13:30:44 -04:00
Russ Taylor
2ee08f6161 Setting Kinemage line radius as half the width, clamped to a minimum of 1.0 2026-04-03 13:30:44 -04:00
Russ Taylor
3608578528 Fixing width code on vectors. Cleaning up color code 2026-04-03 13:30:44 -04:00
Russ Taylor
5114a211fd Adding control over line width. Allowing short forms of list names. Working on passing color through 2026-04-03 13:30:44 -04:00
Russ Taylor
8cc947c998 Adding sphere generation for BallList in Kinemage files. Added reading of radius from list line to enable list-wide specification 2026-04-03 13:30:44 -04:00
Russ Taylor
9105737834 Fixing over-counting of points 2026-04-03 13:30:44 -04:00
Russ Taylor
84bcbd1ca6 Reducing the number of objects and commits 2026-04-03 13:30:44 -04:00
Russ Taylor
144967dbd3 Only reporting an opened file if we get a kinemage 2026-04-03 13:30:44 -04:00
Russ Taylor
bdb33b9398 More work but less fragile 2026-04-03 13:30:44 -04:00
Russ Taylor
ade6ef5631 Continued cleanup 2026-04-03 13:30:44 -04:00
Russ Taylor
3bc60d1d59 Cleaning up 2026-04-03 13:30:44 -04:00
Russ Taylor
4068c45eb4 More cleanup 2026-04-03 13:30:44 -04:00
Russ Taylor
52d0ff4a67 Cleaning up nesting and variables 2026-04-03 13:30:44 -04:00
Russ Taylor
f346d15bef Also draws points for dotLists in Kinemage 2026-04-03 13:30:44 -04:00
Russ Taylor
b2ce1fc6fa Kinemage drag-and-drop handler now shows both lines and ribbons 2026-04-03 13:30:44 -04:00
Russ Taylor
4e331001ef Renaming the Kinemage shape provider pipeline to include the name lines so we can make separate ones for meshes and balls 2026-04-03 13:30:44 -04:00
Russ Taylor
7d67304e4c First working Kinemage extension code that can draw lines from all drag-and-drop kinemages 2026-04-03 13:30:44 -04:00
Russ Taylor
babda601cb Changing name to kinemage 2026-04-03 13:30:44 -04:00
Russ Taylor
8bdfff5e94 Fixing name on KinemageDataProvider 2026-04-03 13:30:44 -04:00
Russ Taylor
be4b408ddc Renaming for clarity 2026-04-03 13:30:44 -04:00
Russ Taylor
230697fbb4 Modifying the kinemage parser to be able to read more than one kinemage entry from the same file. Hooking in a drag and drop handler on .kin files that for now just stores things into a global variable. Adjusting the parsing of kinemage through the original plugin path to handle the new parsing. 2026-04-03 13:30:44 -04:00
Russ Taylor
78ab6b0c95 Duplicating ANVIL structure in a kinemage extension directory as a basis for a new Kinemage loading extension
Trying to stick with the master code, which has changed a lot.
2026-04-03 13:30:20 -04:00
Russ Taylor
1f0c24b58e Initial implementation of converting vectors into lines. Still needs groups, colors, labels, etc. 2026-04-03 13:27:53 -04:00
Russ Taylor
f5c587bfe5 Initial construction of lines from vector lists. Still need to do multiple vector lists, colors, labels, and more. 2026-04-03 13:27:53 -04:00
Russ Taylor
52b1c7e4d9 Removing copied PLY parsing and geometry generation code from KIN file reader. Passing the Kinemage data structure from the parser to the geometry generator. Stubs now in place for KIN with no geometry currently being generated. Also updated the kin.spec.ts file to check a Kinemage file. 2026-04-03 13:27:53 -04:00
Russ Taylor
e76d02bc8c Organizing and commenting list structures. Specifying types where known. 2026-04-03 13:27:53 -04:00
Russ Taylor
481b763049 Moving interface definitions to scheme.ts for Kin file reader. 2026-04-03 13:27:53 -04:00
Russ Taylor
7bfef2ae40 Initial pull-in of NGL Kinemage parser code. It is called by parser.ts and counts of objects are printed. 2026-04-03 13:27:53 -04:00
Russ Taylor
01fe10ebdc Adding KIN loading to mesoscale app 2026-04-03 13:27:53 -04:00
Russ Taylor
4e4b80a7b2 Initial Kinemage commit that copies the PLY files and references to make it possible to load a PLY-formate file from a file with a KIN extension.
Overwriting package-lock.json
2026-04-03 13:27:18 -04:00
38 changed files with 572 additions and 2239 deletions

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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,
};

View File

@@ -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 }),

View File

@@ -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);
});
});

View File

@@ -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

View File

@@ -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
};
});

View File

@@ -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 {

View File

@@ -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}</>;
}
}

View File

@@ -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);

View File

@@ -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');

View File

@@ -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));

View File

@@ -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';
}

View File

@@ -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]];

View File

@@ -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();

View File

@@ -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;
}
},
};
}

View File

@@ -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 AWB angle (°)' }),
omegaMax: PD.Numeric(140, { min: 0, max: 180, step: 1 }, { description: 'Maximum AWB 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);
}

View File

@@ -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);
}

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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') {

View File

@@ -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();

View File

@@ -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;

View File

@@ -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.

View File

@@ -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);
}
}

View File

@@ -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';

View File

@@ -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' };

View File

@@ -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' };

View File

@@ -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]);
});
});

View File

@@ -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)
});
});

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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
}
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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);

View File

@@ -4,4 +4,4 @@
* @author David Sehnal <david.sehnal@gmail.com>
*/
export const VERSION = '0.9.13';
export const VERSION = '0.9.12';