Compare commits

...

53 Commits

Author SHA1 Message Date
Alexander Rose
844c13cd35 2.2.0-dev.1 2021-07-20 21:08:51 -07:00
Alexander Rose
d1c8b92fdf Merge pull request #224 from sukolsak/usdz-export
USDZ export
2021-07-18 09:28:27 -07:00
Sukolsak Sakshuwong
93d33bca80 view USDZ in AR on iOS 2021-07-17 21:41:26 -07:00
Sukolsak Sakshuwong
6550e53414 Merge branch 'master' into usdz-export 2021-07-17 18:50:43 -07:00
Alexander Rose
96dddb0998 updated changelog 2021-07-17 10:54:34 -07:00
Alexander Rose
baa64d8109 handle mononucleotides when guessing component type 2021-07-17 10:42:56 -07:00
David Sehnal
2df145aa8f Merge pull request #231 from molstar/sdf-parser-improvements
Sdf parser improvements
2021-07-16 18:27:16 +02:00
dsehnal
06b9c5f2de change SDF data header parsing
- do not trim <> around field
- store whole line staring with '> ' as data header (without the staring '> ')
2021-07-16 18:25:22 +02:00
dsehnal
e03b689f27 add SdfFormat 2021-07-16 18:04:25 +02:00
Sukolsak Sakshuwong
e4cdcff3ee Merge branch 'master' into usdz-export 2021-07-12 00:11:24 -07:00
Alexander Rose
f73150d074 Merge pull request #225 from molstar/tubular-helices
Add tubularHelices parameter to Cartoon representation
2021-07-11 11:45:38 -07:00
Alexander Rose
451dc12689 cleanup 2021-07-11 11:31:02 -07:00
Alexander Rose
a3fb7762d8 add tubular helices to Cartoon representation 2021-07-10 15:41:54 -07:00
Alexander Rose
3dfafc3202 handle cell angles close to zero in dcd reader 2021-07-10 15:35:53 -07:00
Alexander Rose
4fcea991d3 set default outline scale back to 1 2021-07-10 15:28:49 -07:00
Alexander Rose
0607ed46d1 handle more common ff residue/atom names 2021-07-10 15:28:23 -07:00
Sukolsak Sakshuwong
30d6244e82 add support for USDZ 2021-07-10 05:19:42 -07:00
Sukolsak Sakshuwong
fab8c74365 move quantizeColors to MeshExporter 2021-07-09 13:10:09 -07:00
Sukolsak Sakshuwong
1952922e4e make RenderObjectExporter.getData async 2021-07-09 13:10:09 -07:00
Alexander Rose
1eb351369e 2.1.0 2021-07-05 16:11:46 -07:00
Alexander Rose
701d782485 changelog 2021-07-05 16:07:30 -07:00
Alexander Rose
dc8457c4dc smoother trace normals 2021-07-05 15:59:28 -07:00
Alexander Rose
f104cd4d11 add missing import 2021-07-05 13:47:39 -07:00
Alexander Rose
9b56a6ae65 Merge pull request #221 from molstar/aromatic
Aromatic bond display option
2021-07-05 13:42:11 -07:00
Alexander Rose
2485ad5a2f Merge branch 'master' into aromatic 2021-07-05 13:38:05 -07:00
Alexander Rose
a56716ab6a Merge pull request #222 from molstar/backbone
Backbone representation
2021-07-05 13:37:24 -07:00
Alexander Rose
e0aaaa989e Merge branch 'master' into backbone 2021-07-05 13:34:40 -07:00
Alexander Rose
9ec0f9e736 outline fixes and improvements
- better handle outlines in orthographic mode
- remove unused code
- increase default outline scale to 2
2021-07-05 13:31:00 -07:00
Alexander Rose
47968eeeec fix traceOnly handling in makeElementIgnoreTest 2021-07-04 14:54:52 -07:00
Alexander Rose
9c157b70e1 warning for arrayAreIntersecting/arrayIntersectionSize 2021-07-04 14:50:58 -07:00
Alexander Rose
6d7e4ca227 remove unused import 2021-07-04 14:48:06 -07:00
Alexander Rose
fccd08d2ec add backbone repr
- atomistic and coarse units
2021-07-04 14:39:56 -07:00
Alexander Rose
19bae202d0 handle Vec3.angle edge case 2021-07-04 14:35:46 -07:00
Alexander Rose
4ba0ae24e4 support aromatic bond display as dashes
- add arrayAreIntersecting/arrayIntersectionSize helpers
- prefer reference atoms within rings (also for double/triple bonds)
2021-07-03 23:12:06 -07:00
Alexander Rose
fcf3718d75 better parsing of mol2 bond types 2021-07-03 22:38:29 -07:00
Alexander Rose
df1dd94f1c fix BondType.Names 2021-07-03 22:35:35 -07:00
Alexander Rose
65ba401850 fix repr update for Structure.asParent objects 2021-07-03 22:26:20 -07:00
Alexander Rose
a98f5e1047 fix fxaa antialiasing
- was broken when used with other postprocessing effects
- expose texture.filter
2021-07-03 22:19:51 -07:00
David Sehnal
e5cf97d1ea Merge pull request #217 from sukolsak/fix-cylinder
Fix cylinder orientation
2021-06-28 14:45:38 +02:00
Sukolsak Sakshuwong
1844fc14b2 fix cylinder orientation 2021-06-27 13:14:30 -07:00
dsehnal
d185c0ef34 2.0.7 2021-06-23 12:44:20 +02:00
dsehnal
40a4211e75 fix CIF text encoder edge cases & added test 2021-06-23 12:41:23 +02:00
dsehnal
daa2bbd042 Merge branch 'master' of https://github.com/molstar/molstar 2021-06-21 16:39:25 +02:00
Alexander Rose
ed5b4b27a8 guard against atom_site not being available 2021-06-20 22:55:25 -07:00
Alexander Rose
408ccb4353 fix bond cylinder imposter update issue 2021-06-20 13:54:15 -07:00
Alexander Rose
99e3cd6654 fix image export issues
- handle pre-multiplied alpha
- don't clear draw target unless written to
2021-06-20 13:51:58 -07:00
Alexander Rose
0819ace1dc use CustomProperty.Provider.ref 2021-06-20 13:49:42 -07:00
dsehnal
987c9210bd In-place reordering support for Frame.x/y/z 2021-06-19 12:26:42 +02:00
dsehnal
84fb42a161 Support volumeIndex in Viewer.loadVolumeFromUrl 2021-06-19 11:24:32 +02:00
dsehnal
53d3480701 fix isConnectedTo query 2021-06-15 17:53:40 +02:00
David Sehnal
eb629ef337 Merge pull request #212 from sukolsak/center-export
Geometry export: center exported models
2021-06-14 15:55:16 +02:00
Sukolsak Sakshuwong
c26111e8fb center exported models 2021-06-13 23:32:00 -07:00
dsehnal
4853ff7a1a fix volume streaming channel visibility 2021-06-11 15:19:04 +02:00
87 changed files with 1950 additions and 635 deletions

View File

@@ -3,8 +3,29 @@ 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]
- Add `tubularHelices` parameter to Cartoon representation
- Add `SdfFormat` and update SDF parser to be able to parse data headers according to spec (hopefully :)) #230
- Fix mononucleotides detected as polymer components (#229)
- Set default outline scale back to 1
- Improved DCD reader cell angle handling (intepret near 0 angles as 90 deg)
- Handle more residue/atom names commonly used in force-fields
- Add USDZ support to ``geo-export`` extension.
## [v2.1.0] - 2021-07-05
- Add parameter for to display aromatic bonds as dashes next to solid cylinder/line.
- Add backbone representation
- Fix outline in orthographic mode and set default scale to 2.
## [v2.0.7] - 2021-06-23
- Add ability to specify ``volumeIndex`` in ``Viewer.loadVolumeFromUrl`` to better support Volume Server inputs.
- Support in-place reordering for trajectory ``Frame.x/y/z`` arrays for better memory efficiency.
- Fixed text CIF encoder edge cases (most notably single whitespace not being escaped).
## [v2.0.6] - 2021-06-01
- Add glTF (GLB) and STL support to ``geo-export`` extension.

4
package-lock.json generated
View File

@@ -1,11 +1,11 @@
{
"name": "molstar",
"version": "2.0.6",
"version": "2.2.0-dev.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"version": "2.0.6",
"version": "2.0.7",
"license": "MIT",
"dependencies": {
"@types/argparse": "^1.0.38",

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "2.0.6",
"version": "2.2.0-dev.1",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {

View File

@@ -38,19 +38,6 @@
viewer.loadPdb('7bv2');
viewer.loadEmdb('EMD-30210', { detail: 6 });
// viewer.loadVolumeFromUrl({
// url: 'https://maps.rcsb.org/em/emd-30210/cell?detail=6',
// format: 'dscif',
// isBinary: true
// }, [{
// type: 'relative',
// value: 1,
// color: 0x3377aa
// }], {
// entryId: 'EMD-30210',
// isLazy: true
// });
// viewer.loadAllModelsOrAssemblyFromUrl('https://cs.litemol.org/5ire/full', 'mmcif', false, { representationParams: { theme: { globalName: 'operator-name' } } })
</script>
</body>

View File

@@ -243,7 +243,46 @@ export class Viewer {
}));
}
async loadVolumeFromUrl({ url, format, isBinary }: { url: string, format: BuildInVolumeFormat, isBinary: boolean }, isovalues: VolumeIsovalueInfo[], options?: { entryId?: string, isLazy?: boolean }) {
/**
* @example Load X-ray density from volume server
viewer.loadVolumeFromUrl({
url: 'https://www.ebi.ac.uk/pdbe/densities/x-ray/1tqn/cell?detail=3',
format: 'dscif',
isBinary: true
}, [{
type: 'relative',
value: 1.5,
color: 0x3362B2
}, {
type: 'relative',
value: 3,
color: 0x33BB33,
volumeIndex: 1
}, {
type: 'relative',
value: -3,
color: 0xBB3333,
volumeIndex: 1
}], {
entryId: ['2FO-FC', 'FO-FC'],
isLazy: true
});
* *********************
* @example Load EM density from volume server
viewer.loadVolumeFromUrl({
url: 'https://maps.rcsb.org/em/emd-30210/cell?detail=6',
format: 'dscif',
isBinary: true
}, [{
type: 'relative',
value: 1,
color: 0x3377aa
}], {
entryId: 'EMD-30210',
isLazy: true
});
*/
async loadVolumeFromUrl({ url, format, isBinary }: { url: string, format: BuildInVolumeFormat, isBinary: boolean }, isovalues: VolumeIsovalueInfo[], options?: { entryId?: string | string[], isLazy?: boolean }) {
const plugin = this.plugin;
if (!plugin.dataFormats.get(format)) {
@@ -257,26 +296,28 @@ export class Viewer {
format,
entryId: options?.entryId,
isBinary,
isovalues: isovalues.map(v => ({ alpha: 1, ...v }))
isovalues: isovalues.map(v => ({ alpha: 1, volumeIndex: 0, ...v }))
});
return update.commit();
}
return plugin.dataTransaction(async () => {
const data = await plugin.builders.data.download({ url, isBinary, label: options?.entryId }, { state: { isGhost: true } });
const data = await plugin.builders.data.download({ url, isBinary }, { state: { isGhost: true } });
const parsed = await plugin.dataFormats.get(format)!.parse(plugin, data, { entryId: options?.entryId });
const volume = (parsed.volume || parsed.volumes[0]) as StateObjectSelector<PluginStateObject.Volume.Data>;
if (!volume?.isOk) throw new Error('Failed to parse any volume.');
const firstVolume = (parsed.volume || parsed.volumes[0]) as StateObjectSelector<PluginStateObject.Volume.Data>;
if (!firstVolume?.isOk) throw new Error('Failed to parse any volume.');
const repr = plugin.build().to(volume);
const repr = plugin.build();
for (const iso of isovalues) {
repr.apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.plugin, volume.data!, {
type: 'isosurface',
typeParams: { alpha: iso.alpha ?? 1, isoValue: iso.type === 'absolute' ? { kind: 'absolute', absoluteValue: iso.value } : { kind: 'relative', relativeValue: iso.value } },
color: 'uniform',
colorParams: { value: iso.color }
}));
repr
.to(parsed.volumes?.[iso.volumeIndex ?? 0] ?? parsed.volume)
.apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.plugin, firstVolume.data!, {
type: 'isosurface',
typeParams: { alpha: iso.alpha ?? 1, isoValue: iso.type === 'absolute' ? { kind: 'absolute', absoluteValue: iso.value } : { kind: 'relative', relativeValue: iso.value } },
color: 'uniform',
colorParams: { value: iso.color }
}));
}
await repr.commit();
@@ -296,5 +337,6 @@ export interface VolumeIsovalueInfo {
type: 'absolute' | 'relative',
value: number,
color: Color,
alpha?: number
alpha?: number,
volumeIndex?: number
}

View File

@@ -181,6 +181,6 @@ export const ConfalPyramidsColorThemeProvider: ColorTheme.Provider<ConfalPyramid
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models.some(m => ConfalPyramids.isApplicable(m)),
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? ConfalPyramidsProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
detach: (data) => data.structure && data.structure.models[0].customProperties.reference(ConfalPyramidsProvider.descriptor, false)
detach: (data) => data.structure && ConfalPyramidsProvider.ref(data.structure.models[0], false)
}
};

View File

@@ -5,6 +5,7 @@
*/
import { getStyle } from '../../mol-gl/renderer';
import { Box3D } from '../../mol-math/geometry';
import { PluginComponent } from '../../mol-plugin-state/component';
import { PluginContext } from '../../mol-plugin/context';
import { Task } from '../../mol-task';
@@ -12,15 +13,17 @@ import { PluginStateObject } from '../../mol-plugin-state/objects';
import { StateSelection } from '../../mol-state';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { SetUtils } from '../../mol-util/set';
import { ObjExporter } from './obj-exporter';
import { GlbExporter } from './glb-exporter';
import { ObjExporter } from './obj-exporter';
import { StlExporter } from './stl-exporter';
import { UsdzExporter } from './usdz-exporter';
export const GeometryParams = {
format: PD.Select('glb', [
['glb', 'glTF 2.0 Binary (.glb)'],
['stl', 'Stl (.stl)'],
['obj', 'Wavefront (.obj)']
['obj', 'Wavefront (.obj)'],
['usdz', 'Universal Scene Description (.usdz)']
])
};
@@ -43,17 +46,22 @@ export class GeometryControls extends PluginComponent {
const renderObjects = this.plugin.canvas3d?.getRenderObjects()!;
const filename = this.getFilename();
let renderObjectExporter: ObjExporter | GlbExporter | StlExporter;
const style = getStyle(this.plugin.canvas3d?.props.renderer.style!);
const boundingSphere = this.plugin.canvas3d?.boundingSphereVisible!;
const boundingBox = Box3D.fromSphere3D(Box3D(), boundingSphere);
let renderObjectExporter: GlbExporter | ObjExporter | StlExporter | UsdzExporter;
switch (this.behaviors.params.value.format) {
case 'obj':
renderObjectExporter = new ObjExporter(filename);
break;
case 'glb':
const style = getStyle(this.plugin.canvas3d?.props.renderer.style!);
renderObjectExporter = new GlbExporter(style);
renderObjectExporter = new GlbExporter(style, boundingBox);
break;
case 'obj':
renderObjectExporter = new ObjExporter(filename, boundingBox);
break;
case 'stl':
renderObjectExporter = new StlExporter();
renderObjectExporter = new StlExporter(boundingBox);
break;
case 'usdz':
renderObjectExporter = new UsdzExporter(style, boundingBox, boundingSphere.radius);
break;
default: throw new Error('Unsupported format.');
}

View File

@@ -8,6 +8,7 @@ import { BaseValues } from '../../mol-gl/renderable/schema';
import { Style } from '../../mol-gl/renderer';
import { asciiWrite } from '../../mol-io/common/ascii';
import { IsNativeEndianLittle, flipByteOrder } from '../../mol-io/common/binary';
import { Box3D } from '../../mol-math/geometry';
import { Vec3, Mat4 } from '../../mol-math/linear-algebra';
import { PLUGIN_VERSION } from '../../mol-plugin/version';
import { RuntimeContext } from '../../mol-task';
@@ -41,6 +42,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
private bufferViews: Record<string, any>[] = [];
private binaryBuffer: ArrayBuffer[] = [];
private byteOffset = 0;
private centerTransform: Mat4;
private static vec3MinMax(a: NumberArray) {
const min: number[] = [Infinity, Infinity, Infinity];
@@ -252,16 +254,17 @@ export class GlbExporter extends MeshExporter<GlbData> {
}
// node
const node: Record<string, any> = {
mesh: meshIndex!
};
Mat4.fromArray(t, aTransform, instanceIndex * 16);
if (!Mat4.isIdentity(t)) node.matrix = t.slice();
Mat4.mul(t, this.centerTransform, t);
const node: Record<string, any> = {
mesh: meshIndex!,
matrix: t.slice()
};
this.nodes.push(node);
}
}
getData() {
async getData() {
const binaryBufferLength = this.byteOffset;
const gltf = {
@@ -331,10 +334,14 @@ export class GlbExporter extends MeshExporter<GlbData> {
}
async getBlob(ctx: RuntimeContext) {
return new Blob([this.getData().glb], { type: 'model/gltf-binary' });
return new Blob([(await this.getData()).glb], { type: 'model/gltf-binary' });
}
constructor(private style: Style) {
constructor(private style: Style, boundingBox: Box3D) {
super();
const tmpV = Vec3();
Vec3.add(tmpV, boundingBox.min, boundingBox.max);
Vec3.scale(tmpV, tmpV, -0.5);
this.centerTransform = Mat4.fromTranslation(Mat4(), tmpV);
}
}

View File

@@ -4,6 +4,7 @@
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
*/
import { sort, arraySwap } from '../../mol-data/util';
import { GraphicsRenderObject } from '../../mol-gl/render-object';
import { MeshValues } from '../../mol-gl/renderable/mesh';
import { LinesValues } from '../../mol-gl/renderable/lines';
@@ -22,6 +23,7 @@ import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder';
import { sizeDataFactor } from '../../mol-geo/geometry/size-data';
import { Vec3 } from '../../mol-math/linear-algebra';
import { RuntimeContext } from '../../mol-task';
import { Color } from '../../mol-util/color/color';
import { decodeFloatRGB } from '../../mol-util/float-packing';
import { RenderObjectExporter, RenderObjectExportData } from './render-object-exporter';
@@ -111,6 +113,70 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
return interpolated.array;
}
protected static quantizeColors(colorArray: Uint8Array, vertexCount: number) {
if (vertexCount <= 1024) return;
const rgb = Vec3();
const min = Vec3();
const max = Vec3();
const sum = Vec3();
const colorMap = new Map<Color, Color>();
const colorComparers = [
(colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[0] - Color.toVec3(rgb, colors[j])[0]),
(colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[1] - Color.toVec3(rgb, colors[j])[1]),
(colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[2] - Color.toVec3(rgb, colors[j])[2]),
];
const medianCut = (colors: Color[], l: number, r: number, depth: number) => {
if (l > r) return;
if (l === r || depth >= 10) {
// Find the average color.
Vec3.set(sum, 0, 0, 0);
for (let i = l; i <= r; ++i) {
Color.toVec3(rgb, colors[i]);
Vec3.add(sum, sum, rgb);
}
Vec3.round(rgb, Vec3.scale(rgb, sum, 1 / (r - l + 1)));
const averageColor = Color.fromArray(rgb, 0);
for (let i = l; i <= r; ++i) colorMap.set(colors[i], averageColor);
return;
}
// Find the color channel with the greatest range.
Vec3.set(min, 255, 255, 255);
Vec3.set(max, 0, 0, 0);
for (let i = l; i <= r; ++i) {
Color.toVec3(rgb, colors[i]);
for (let j = 0; j < 3; ++j) {
Vec3.min(min, min, rgb);
Vec3.max(max, max, rgb);
}
}
let k = 0;
if (max[1] - min[1] > max[k] - min[k]) k = 1;
if (max[2] - min[2] > max[k] - min[k]) k = 2;
sort(colors, l, r + 1, colorComparers[k], arraySwap);
const m = (l + r) >> 1;
medianCut(colors, l, m, depth + 1);
medianCut(colors, m + 1, r, depth + 1);
};
// Create an array of unique colors and use the median cut algorithm.
const colorSet = new Set<Color>();
for (let i = 0; i < vertexCount; ++i) {
colorSet.add(Color.fromArray(colorArray, i * 3));
}
const colors = Array.from(colorSet);
medianCut(colors, 0, colors.length - 1, 0);
// Map actual colors to quantized colors.
for (let i = 0; i < vertexCount; ++i) {
const color = colorMap.get(Color.fromArray(colorArray, i * 3));
Color.toArray(color!, colorArray, i * 3);
}
}
protected static getInstance(input: AddMeshInput, instanceIndex: number) {
const { mesh, meshes } = input;
if (mesh !== undefined) {
@@ -278,7 +344,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
}
}
abstract getData(): D;
abstract getData(ctx: RuntimeContext): Promise<D>;
abstract getBlob(ctx: RuntimeContext): Promise<Blob>;
}

View File

@@ -4,8 +4,8 @@
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
*/
import { sort, arraySwap } from '../../mol-data/util';
import { asciiWrite } from '../../mol-io/common/ascii';
import { Box3D } from '../../mol-math/geometry';
import { Vec3, Mat3, Mat4 } from '../../mol-math/linear-algebra';
import { RuntimeContext } from '../../mol-task';
import { StringBuilder } from '../../mol-util';
@@ -35,6 +35,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
private currentColor: Color | undefined;
private currentAlpha: number | undefined;
private materialSet = new Set<string>();
private centerTransform: Mat4;
private updateMaterial(color: Color, alpha: number) {
if (this.currentColor === color && this.currentAlpha === alpha) return;
@@ -67,70 +68,6 @@ export class ObjExporter extends MeshExporter<ObjData> {
}
}
private static quantizeColors(colorArray: Uint8Array, vertexCount: number) {
if (vertexCount <= 1024) return;
const rgb = Vec3();
const min = Vec3();
const max = Vec3();
const sum = Vec3();
const colorMap = new Map<Color, Color>();
const colorComparers = [
(colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[0] - Color.toVec3(rgb, colors[j])[0]),
(colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[1] - Color.toVec3(rgb, colors[j])[1]),
(colors: Color[], i: number, j: number) => (Color.toVec3(rgb, colors[i])[2] - Color.toVec3(rgb, colors[j])[2]),
];
const medianCut = (colors: Color[], l: number, r: number, depth: number) => {
if (l > r) return;
if (l === r || depth >= 10) {
// Find the average color.
Vec3.set(sum, 0, 0, 0);
for (let i = l; i <= r; ++i) {
Color.toVec3(rgb, colors[i]);
Vec3.add(sum, sum, rgb);
}
Vec3.round(rgb, Vec3.scale(rgb, sum, 1 / (r - l + 1)));
const averageColor = Color.fromArray(rgb, 0);
for (let i = l; i <= r; ++i) colorMap.set(colors[i], averageColor);
return;
}
// Find the color channel with the greatest range.
Vec3.set(min, 255, 255, 255);
Vec3.set(max, 0, 0, 0);
for (let i = l; i <= r; ++i) {
Color.toVec3(rgb, colors[i]);
for (let j = 0; j < 3; ++j) {
Vec3.min(min, min, rgb);
Vec3.max(max, max, rgb);
}
}
let k = 0;
if (max[1] - min[1] > max[k] - min[k]) k = 1;
if (max[2] - min[2] > max[k] - min[k]) k = 2;
sort(colors, l, r + 1, colorComparers[k], arraySwap);
const m = (l + r) >> 1;
medianCut(colors, l, m, depth + 1);
medianCut(colors, m + 1, r, depth + 1);
};
// Create an array of unique colors and use the median cut algorithm.
const colorSet = new Set<Color>();
for (let i = 0; i < vertexCount; ++i) {
colorSet.add(Color.fromArray(colorArray, i * 3));
}
const colors = Array.from(colorSet);
medianCut(colors, 0, colors.length - 1, 0);
// Map actual colors to quantized colors.
for (let i = 0; i < vertexCount; ++i) {
const color = colorMap.get(Color.fromArray(colorArray, i * 3));
Color.toArray(color!, colorArray, i * 3);
}
}
protected async addMeshWithColors(input: AddMeshInput) {
const { mesh, values, isGeoTexture, webgl, ctx } = input;
@@ -163,6 +100,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
const { vertices, normals, indices, groups, vertexCount, drawCount } = ObjExporter.getInstance(input, instanceIndex);
Mat4.fromArray(t, aTransform, instanceIndex * 16);
Mat4.mul(t, this.centerTransform, t);
mat3directionTransform(n, t);
// position
@@ -253,7 +191,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
}
}
getData() {
async getData() {
return {
obj: StringBuilder.getString(this.obj),
mtl: StringBuilder.getString(this.mtl)
@@ -261,7 +199,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
}
async getBlob(ctx: RuntimeContext) {
const { obj, mtl } = this.getData();
const { obj, mtl } = await this.getData();
const objData = new Uint8Array(obj.length);
asciiWrite(objData, obj);
const mtlData = new Uint8Array(mtl.length);
@@ -273,8 +211,12 @@ export class ObjExporter extends MeshExporter<ObjData> {
return new Blob([await zip(ctx, zipDataObj)], { type: 'application/zip' });
}
constructor(private filename: string) {
constructor(private filename: string, boundingBox: Box3D) {
super();
StringBuilder.writeSafe(this.obj, `mtllib ${filename}.mtl\n`);
const tmpV = Vec3();
Vec3.add(tmpV, boundingBox.min, boundingBox.max);
Vec3.scale(tmpV, tmpV, -0.5);
this.centerTransform = Mat4.fromTranslation(Mat4(), tmpV);
}
}

View File

@@ -9,12 +9,12 @@ import { WebGLContext } from '../../mol-gl/webgl/context';
import { RuntimeContext } from '../../mol-task';
export type RenderObjectExportData = {
[k: string]: string | Uint8Array | undefined
[k: string]: string | Uint8Array | ArrayBuffer | undefined
}
export interface RenderObjectExporter<D extends RenderObjectExportData> {
readonly fileExtension: string
add(renderObject: GraphicsRenderObject, webgl: WebGLContext, ctx: RuntimeContext): Promise<void> | undefined
getData(): D
getData(ctx: RuntimeContext): Promise<D>
getBlob(ctx: RuntimeContext): Promise<Blob>
}

View File

@@ -5,6 +5,7 @@
*/
import { asciiWrite } from '../../mol-io/common/ascii';
import { Box3D } from '../../mol-math/geometry';
import { Vec3, Mat4 } from '../../mol-math/linear-algebra';
import { PLUGIN_VERSION } from '../../mol-plugin/version';
import { RuntimeContext } from '../../mol-task';
@@ -26,6 +27,7 @@ export class StlExporter extends MeshExporter<StlData> {
readonly fileExtension = 'stl';
private triangleBuffers: ArrayBuffer[] = [];
private triangleCount = 0;
private centerTransform: Mat4;
protected async addMeshWithColors(input: AddMeshInput) {
const { values, isGeoTexture, ctx } = input;
@@ -46,6 +48,7 @@ export class StlExporter extends MeshExporter<StlData> {
const { vertices, indices, vertexCount, drawCount } = StlExporter.getInstance(input, instanceIndex);
Mat4.fromArray(t, aTransform, instanceIndex * 16);
Mat4.mul(t, this.centerTransform, t);
// position
const vertexArray = new Float32Array(vertexCount * 3);
@@ -86,7 +89,7 @@ export class StlExporter extends MeshExporter<StlData> {
}
}
getData() {
async getData() {
const stl = new Uint8Array(84 + 50 * this.triangleCount);
asciiWrite(stl, `Exported from Mol* ${PLUGIN_VERSION}`);
@@ -103,6 +106,14 @@ export class StlExporter extends MeshExporter<StlData> {
}
async getBlob(ctx: RuntimeContext) {
return new Blob([this.getData().stl], { type: 'model/stl' });
return new Blob([(await this.getData()).stl], { type: 'model/stl' });
}
constructor(boundingBox: Box3D) {
super();
const tmpV = Vec3();
Vec3.add(tmpV, boundingBox.min, boundingBox.max);
Vec3.scale(tmpV, tmpV, -0.5);
this.centerTransform = Mat4.fromTranslation(Mat4(), tmpV);
}
}

View File

@@ -7,7 +7,7 @@
import { merge } from 'rxjs';
import { CollapsableControls, CollapsableState } from '../../mol-plugin-ui/base';
import { Button } from '../../mol-plugin-ui/controls/common';
import { GetAppSvg, CubeSendSvg } from '../../mol-plugin-ui/controls/icons';
import { GetAppSvg, CubeScanSvg, CubeSendSvg } from '../../mol-plugin-ui/controls/icons';
import { ParameterControls } from '../../mol-plugin-ui/controls/parameters';
import { download } from '../../mol-util/download';
import { GeometryParams, GeometryControls } from './controls';
@@ -18,6 +18,7 @@ interface State {
export class GeometryExporterUI extends CollapsableControls<{}, State> {
private _controls: GeometryControls | undefined;
private isARSupported: boolean | undefined;
get controls() {
return this._controls || (this._controls = new GeometryControls(this.plugin));
@@ -32,6 +33,9 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> {
}
protected renderControls(): JSX.Element {
if (this.isARSupported === undefined) {
this.isARSupported = !!document.createElement('a').relList?.supports?.('ar');
}
const ctrl = this.controls;
return <>
<ParameterControls
@@ -45,6 +49,13 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> {
disabled={this.state.busy || !this.plugin.canvas3d?.reprCount.value}>
Save
</Button>
{this.isARSupported && ctrl.behaviors.params.value.format === 'usdz' &&
<Button icon={CubeScanSvg}
onClick={this.viewInAR} style={{ marginTop: 1 }}
disabled={this.state.busy || !this.plugin.canvas3d?.reprCount.value}>
View in AR
</Button>
}
</>;
}
@@ -75,4 +86,22 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> {
this.setState({ busy: false });
}
}
viewInAR = async () => {
try {
this.setState({ busy: true });
const data = await this.controls.exportGeometry();
this.setState({ busy: false });
const a = document.createElement('a');
a.rel = 'ar';
a.href = URL.createObjectURL(data.blob);
// For in-place viewing of USDZ on iOS, the link must contain a single child that is either an img or picture.
// https://webkit.org/blog/8421/viewing-augmented-reality-assets-in-safari-for-ios/
a.appendChild(document.createElement('img'));
setTimeout(() => URL.revokeObjectURL(a.href), 4E4); // 40s
setTimeout(() => a.dispatchEvent(new MouseEvent('click')));
} catch {
this.setState({ busy: false });
}
}
}

View File

@@ -0,0 +1,262 @@
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
*/
import { Style } from '../../mol-gl/renderer';
import { asciiWrite } from '../../mol-io/common/ascii';
import { Box3D } from '../../mol-math/geometry';
import { Vec3, Mat3, Mat4 } from '../../mol-math/linear-algebra';
import { PLUGIN_VERSION } from '../../mol-plugin/version';
import { RuntimeContext } from '../../mol-task';
import { StringBuilder } from '../../mol-util';
import { Color } from '../../mol-util/color/color';
import { zip } from '../../mol-util/zip/zip';
import { MeshExporter, AddMeshInput } from './mesh-exporter';
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
const v3fromArray = Vec3.fromArray;
const v3transformMat4 = Vec3.transformMat4;
const v3transformMat3 = Vec3.transformMat3;
const mat3directionTransform = Mat3.directionTransform;
// https://graphics.pixar.com/usd/docs/index.html
export type UsdzData = {
usdz: ArrayBuffer
}
export class UsdzExporter extends MeshExporter<UsdzData> {
readonly fileExtension = 'usdz';
private meshes: string[] = [];
private materials: string[] = [];
private materialSet = new Set<number>();
private centerTransform: Mat4;
private static getMaterialKey(color: Color, alpha: number) {
return color * 256 + Math.round(alpha * 255);
}
private addMaterial(color: Color, alpha: number) {
const materialKey = UsdzExporter.getMaterialKey(color, alpha);
if (this.materialSet.has(materialKey)) return;
this.materialSet.add(materialKey);
const [r, g, b] = Color.toRgbNormalized(color);
this.materials.push(`
def Material "material${materialKey}"
{
token outputs:surface.connect = </material${materialKey}/shader.outputs:surface>
def Shader "shader"
{
uniform token info:id = "UsdPreviewSurface"
color3f inputs:diffuseColor = (${r},${g},${b})
float inputs:opacity = ${alpha}
float inputs:metallic = ${this.style.metalness}
float inputs:roughness = ${this.style.roughness}
token outputs:surface
}
}
`);
}
protected async addMeshWithColors(input: AddMeshInput) {
const { mesh, values, isGeoTexture, webgl, ctx } = input;
const t = Mat4();
const n = Mat3();
const tmpV = Vec3();
const stride = isGeoTexture ? 4 : 3;
const groupCount = values.uGroupCount.ref.value;
const colorType = values.dColorType.ref.value;
const tColor = values.tColor.ref.value.array;
const uAlpha = values.uAlpha.ref.value;
const dTransparency = values.dTransparency.ref.value;
const tTransparency = values.tTransparency.ref.value;
const aTransform = values.aTransform.ref.value;
const instanceCount = values.uInstanceCount.ref.value;
let interpolatedColors: Uint8Array;
if (colorType === 'volume' || colorType === 'volumeInstance') {
interpolatedColors = UsdzExporter.getInterpolatedColors(mesh!.vertices, mesh!.vertexCount, values, stride, colorType, webgl!);
UsdzExporter.quantizeColors(interpolatedColors, mesh!.vertexCount);
}
await ctx.update({ isIndeterminate: false, current: 0, max: instanceCount });
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
if (ctx.shouldUpdate) await ctx.update({ current: instanceIndex + 1 });
const { vertices, normals, indices, groups, vertexCount, drawCount } = UsdzExporter.getInstance(input, instanceIndex);
Mat4.fromArray(t, aTransform, instanceIndex * 16);
Mat4.mul(t, this.centerTransform, t);
mat3directionTransform(n, t);
const vertexBuilder = StringBuilder.create();
const normalBuilder = StringBuilder.create();
const indexBuilder = StringBuilder.create();
// position
for (let i = 0; i < vertexCount; ++i) {
v3transformMat4(tmpV, v3fromArray(tmpV, vertices, i * stride), t);
StringBuilder.writeSafe(vertexBuilder, (i === 0) ? '(' : ',(');
StringBuilder.writeFloat(vertexBuilder, tmpV[0], 10000);
StringBuilder.writeSafe(vertexBuilder, ',');
StringBuilder.writeFloat(vertexBuilder, tmpV[1], 10000);
StringBuilder.writeSafe(vertexBuilder, ',');
StringBuilder.writeFloat(vertexBuilder, tmpV[2], 10000);
StringBuilder.writeSafe(vertexBuilder, ')');
}
// normal
for (let i = 0; i < vertexCount; ++i) {
v3transformMat3(tmpV, v3fromArray(tmpV, normals, i * stride), n);
StringBuilder.writeSafe(normalBuilder, (i === 0) ? '(' : ',(');
StringBuilder.writeFloat(normalBuilder, tmpV[0], 100);
StringBuilder.writeSafe(normalBuilder, ',');
StringBuilder.writeFloat(normalBuilder, tmpV[1], 100);
StringBuilder.writeSafe(normalBuilder, ',');
StringBuilder.writeFloat(normalBuilder, tmpV[2], 100);
StringBuilder.writeSafe(normalBuilder, ')');
}
// face
for (let i = 0; i < drawCount; ++i) {
const v = isGeoTexture ? i : indices![i];
if (i > 0) StringBuilder.writeSafe(indexBuilder, ',');
StringBuilder.writeInteger(indexBuilder, v);
}
// color
const faceIndicesByMaterial = new Map<number, number[]>();
for (let i = 0; i < drawCount; i += 3) {
let color: Color;
switch (colorType) {
case 'uniform':
color = Color.fromNormalizedArray(values.uColor.ref.value, 0);
break;
case 'instance':
color = Color.fromArray(tColor, instanceIndex * 3);
break;
case 'group': {
const group = isGeoTexture ? UsdzExporter.getGroup(groups, i) : groups[indices![i]];
color = Color.fromArray(tColor, group * 3);
break;
}
case 'groupInstance': {
const group = isGeoTexture ? UsdzExporter.getGroup(groups, i) : groups[indices![i]];
color = Color.fromArray(tColor, (instanceIndex * groupCount + group) * 3);
break;
}
case 'vertex':
color = Color.fromArray(tColor, indices![i] * 3);
break;
case 'vertexInstance':
color = Color.fromArray(tColor, (instanceIndex * vertexCount + indices![i]) * 3);
break;
case 'volume':
color = Color.fromArray(interpolatedColors!, (isGeoTexture ? i : indices![i]) * 3);
break;
case 'volumeInstance':
color = Color.fromArray(interpolatedColors!, (instanceIndex * vertexCount + (isGeoTexture ? i : indices![i])) * 3);
break;
default: throw new Error('Unsupported color type.');
}
let alpha = uAlpha;
if (dTransparency) {
const group = isGeoTexture ? UsdzExporter.getGroup(groups, i) : groups[indices![i]];
const transparency = tTransparency.array[instanceIndex * groupCount + group] / 255;
alpha *= 1 - transparency;
}
this.addMaterial(color, alpha);
const materialKey = UsdzExporter.getMaterialKey(color, alpha);
let faceIndices = faceIndicesByMaterial.get(materialKey);
if (faceIndices === undefined) {
faceIndices = [];
faceIndicesByMaterial.set(materialKey, faceIndices);
}
faceIndices.push(i / 3);
}
// If this mesh uses only one material, bind it to the material directly.
// Otherwise, use GeomSubsets to bind it to multiple materials.
let materialBinding: string;
if (faceIndicesByMaterial.size === 1) {
const materialKey = faceIndicesByMaterial.keys().next().value;
materialBinding = `rel material:binding = </material${materialKey}>`;
} else {
const geomSubsets: string[] = [];
faceIndicesByMaterial.forEach((faceIndices: number[], materialKey: number) => {
geomSubsets.push(`
def GeomSubset "g${materialKey}"
{
uniform token elementType = "face"
uniform token familyName = "materialBind"
int[] indices = [${faceIndices.join(',')}]
rel material:binding = </material${materialKey}>
}
`);
});
materialBinding = geomSubsets.join('');
}
this.meshes.push(`
def Mesh "mesh${this.meshes.length}"
{
int[] faceVertexCounts = [${new Array(drawCount / 3).fill(3).join(',')}]
int[] faceVertexIndices = [${StringBuilder.getString(indexBuilder)}]
point3f[] points = [${StringBuilder.getString(vertexBuilder)}]
normal3f[] primvars:normals = [${StringBuilder.getString(normalBuilder)}] (
interpolation = "vertex"
)
uniform token subdivisionScheme = "none"
${materialBinding}
}
`);
}
}
async getData(ctx: RuntimeContext) {
const header = `#usda 1.0
(
customLayerData = {
string creator = "Mol* ${PLUGIN_VERSION}"
}
metersPerUnit = 1
)
`;
const usda = [header, ...this.materials, ...this.meshes].join('');
const usdaData = new Uint8Array(usda.length);
asciiWrite(usdaData, usda);
const zipDataObj = {
['model.usda']: usdaData
};
return {
usdz: await zip(ctx, zipDataObj, true)
};
}
async getBlob(ctx: RuntimeContext) {
const { usdz } = await this.getData(ctx);
return new Blob([usdz], { type: 'model/vnd.usdz+zip' });
}
constructor(private style: Style, boundingBox: Box3D, radius: number) {
super();
const t = Mat4();
// scale the model so that it fits within 1 meter
Mat4.fromUniformScaling(t, Math.min(1 / (radius * 2), 1));
// translate the model so that it sits on the ground plane (y = 0)
Mat4.translate(t, t, Vec3.create(
-(boundingBox.min[0] + boundingBox.max[0]) / 2,
-boundingBox.min[1],
-(boundingBox.min[2] + boundingBox.max[2]) / 2
));
this.centerTransform = t;
}
}

View File

@@ -115,6 +115,6 @@ export const StructureQualityReportColorThemeProvider: ColorTheme.Provider<Param
isApplicable: (ctx: ThemeDataContext) => StructureQualityReport.isApplicable(ctx.structure?.models[0]),
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? StructureQualityReportProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
detach: (data) => data.structure && data.structure.models[0].customProperties.reference(StructureQualityReportProvider.descriptor, false)
detach: (data) => data.structure && StructureQualityReportProvider.ref(data.structure.models[0], false)
}
};

View File

@@ -109,6 +109,6 @@ export const AssemblySymmetryClusterColorThemeProvider: ColorTheme.Provider<Asse
isApplicable: (ctx: ThemeDataContext) => AssemblySymmetry.isApplicable(ctx.structure),
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? AssemblySymmetryProvider.attach(ctx, data.structure, void 0, true) : Promise.resolve(),
detach: (data) => data.structure && data.structure.customPropertyDescriptors.reference(AssemblySymmetryProvider.descriptor, false)
detach: (data) => data.structure && AssemblySymmetryProvider.ref(data.structure, false)
}
};

View File

@@ -77,6 +77,6 @@ export const DensityFitColorThemeProvider: ColorTheme.Provider<{}, ValidationRep
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ValidationReport.isApplicable(ctx.structure.models[0]) && Model.isFromXray(ctx.structure.models[0]) && Model.probablyHasDensityMap(ctx.structure.models[0]),
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? ValidationReportProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
detach: (data) => data.structure && data.structure.models[0].customProperties.reference(ValidationReportProvider.descriptor, false)
detach: (data) => data.structure && ValidationReportProvider.ref(data.structure.models[0], false)
}
};

View File

@@ -115,6 +115,6 @@ export const GeometryQualityColorThemeProvider: ColorTheme.Provider<GeometricQua
isApplicable: (ctx: ThemeDataContext) => ValidationReport.isApplicable(ctx.structure?.models[0]),
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? ValidationReportProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
detach: (data) => data.structure && data.structure.models[0].customProperties.reference(ValidationReportProvider.descriptor, false)
detach: (data) => data.structure && ValidationReportProvider.ref(data.structure.models[0], false)
}
};

View File

@@ -68,6 +68,6 @@ export const RandomCoilIndexColorThemeProvider: ColorTheme.Provider<{}, Validati
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ValidationReport.isApplicable(ctx.structure.models[0]) && Model.isFromNmr(ctx.structure.models[0]),
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? ValidationReportProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
detach: (data) => data.structure && data.structure.models[0].customProperties.reference(ValidationReportProvider.descriptor, false)
detach: (data) => data.structure && ValidationReportProvider.ref(data.structure.models[0], false)
}
};

View File

@@ -364,8 +364,9 @@ function updateClip(camera: Camera) {
near = Math.max(Math.min(radiusMax, 5), near);
far = Math.max(5, far);
} else {
near = Math.max(0, near);
far = Math.max(0, far);
// not too close to 0 as it causes issues with outline rendering
near = Math.max(Math.min(radiusMax, 5), near);
far = Math.max(5, far);
}
if (near === far) {

View File

@@ -242,6 +242,7 @@ interface Canvas3D {
requestCameraReset(options?: { durationMs?: number, snapshot?: Camera.SnapshotProvider }): void
readonly camera: Camera
readonly boundingSphere: Readonly<Sphere3D>
readonly boundingSphereVisible: Readonly<Sphere3D>
setProps(props: PartialCanvas3DProps | ((old: Canvas3DProps) => Partial<Canvas3DProps> | void), doNotRequestDraw?: boolean /* = false */): void
getImagePass(props: Partial<ImageProps>): ImagePass
getRenderObjects(): GraphicsRenderObject[]
@@ -717,6 +718,7 @@ namespace Canvas3D {
},
camera,
boundingSphere: scene.boundingSphere,
boundingSphereVisible: scene.boundingSphereVisible,
get notifyDidDraw() { return notifyDidDraw; },
set notifyDidDraw(v: boolean) { notifyDidDraw = v; },
didDraw,

View File

@@ -290,7 +290,7 @@ export class DrawPass {
renderer.setViewport(x, y, width, height);
renderer.update(camera);
if (transparentBackground) {
if (transparentBackground && !antialiasingEnabled && toDrawingBuffer) {
this.drawTarget.bind();
renderer.clear(false);
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2020 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>
*/
@@ -103,7 +103,9 @@ export class ImagePass {
} else {
this.webgl.readPixels(viewport.x, height - viewport.y - viewport.height, w, h, array);
}
PixelData.flipY({ array, width: w, height: h });
const pixelData = PixelData.create(array, w, h);
PixelData.flipY(pixelData);
PixelData.divideByAlpha(pixelData);
return new ImageData(new Uint8ClampedArray(array), w, h);
}
}

View File

@@ -306,7 +306,8 @@ export class PostprocessingPass {
this.nSamples = 1;
this.blurKernelSize = 1;
this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'nearest');
// needs to be linear for anti-aliasing pass
this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
this.outlinesTarget = webgl.createRenderTarget(width, height, false);
this.outlinesRenderable = getOutlinesRenderable(webgl, depthTexture);
@@ -433,8 +434,12 @@ export class PostprocessingPass {
}
if (props.outline.name === 'on') {
const factor = Math.pow(1000, props.outline.params.threshold) / 1000;
const maxPossibleViewZDiff = factor * (camera.far - camera.near);
let { threshold } = props.outline.params;
// orthographic needs lower threshold
if (camera.state.mode === 'orthographic') threshold /= 5;
const factor = Math.pow(1000, threshold) / 1000;
// use radiusMax for stable outlines when zooming
const maxPossibleViewZDiff = factor * camera.state.radiusMax;
const outlineScale = props.outline.params.scale - 1;
ValueCell.updateIfChanged(this.outlinesRenderable.values.uNear, camera.near);
@@ -442,7 +447,6 @@ export class PostprocessingPass {
ValueCell.updateIfChanged(this.outlinesRenderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
ValueCell.updateIfChanged(this.renderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
ValueCell.updateIfChanged(this.renderable.values.uOutlineThreshold, props.outline.params.threshold);
if (this.renderable.values.dOutlineScale.ref.value !== outlineScale) { needsUpdateMain = true; }
ValueCell.updateIfChanged(this.renderable.values.dOutlineScale, outlineScale);
}

View File

@@ -31,6 +31,7 @@ function setCylinderMat(m: Mat4, start: Vec3, dir: Vec3, length: number, matchDi
// ensure the direction used to create the rotation is always pointing in the same
// direction so the triangles of adjacent cylinder will line up
if (matchDir) Vec3.matchDirection(tmpUp, up, tmpCylinderMatDir);
else Vec3.copy(tmpUp, up);
Mat4.fromScaling(tmpCylinderMatScale, Vec3.set(tmpCylinderScale, 1, length, 1));
Vec3.makeRotation(tmpCylinderMatRot, tmpUp, tmpCylinderMatDir);
Mat4.mul(m, tmpCylinderMatRot, tmpCylinderMatScale);

View File

@@ -26,9 +26,6 @@ uniform bool uTransparentBackground;
uniform float uOcclusionBias;
uniform float uOcclusionRadius;
uniform float uOutlineScale;
uniform float uOutlineThreshold;
uniform float uMaxPossibleViewZDiff;
const vec3 occlusionColor = vec3(0.0);

View File

@@ -186,6 +186,7 @@ export interface Texture {
readonly format: number
readonly internalFormat: number
readonly type: number
readonly filter: number
getWidth: () => number
getHeight: () => number
@@ -326,6 +327,7 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension
format,
internalFormat,
type,
filter,
getWidth: () => width,
getHeight: () => height,
@@ -415,6 +417,7 @@ export function createNullTexture(gl?: GLRenderingContext): Texture {
format: 0,
internalFormat: 0,
type: 0,
filter: 0,
getWidth: () => 0,
getHeight: () => 0,

View File

@@ -22,8 +22,8 @@ M END
> <DATABASE_NAME>
drugbank
> <SMILES>
[O-]P([O-])([O-])=O
> 5225 <TEST_FIELD>
whatever
> <INCHI_IDENTIFIER>
InChI=1S/H3O4P/c1-5(2,3)4/h(H3,1,2,3,4)/p-3
@@ -362,22 +362,25 @@ describe('sdf reader', () => {
expect(bonds.atomIdxB.value(3)).toBe(5);
expect(bonds.order.value(3)).toBe(1);
expect(dataItems.dataHeader.value(0)).toBe('DATABASE_ID');
expect(dataItems.dataHeader.value(0)).toBe('<DATABASE_ID>');
expect(dataItems.data.value(0)).toBe('0');
expect(dataItems.dataHeader.value(1)).toBe('DATABASE_NAME');
expect(dataItems.dataHeader.value(1)).toBe('<DATABASE_NAME>');
expect(dataItems.data.value(1)).toBe('drugbank');
expect(dataItems.dataHeader.value(31)).toBe('SYNONYMS');
expect(dataItems.dataHeader.value(2)).toBe('5225 <TEST_FIELD>');
expect(dataItems.data.value(2)).toBe('whatever');
expect(dataItems.dataHeader.value(31)).toBe('<SYNONYMS>');
expect(dataItems.data.value(31)).toBe('Orthophosphate; Phosphate');
expect(compound1.dataItems.data.value(0)).toBe('0');
expect(compound2.dataItems.data.value(0)).toBe('1');
expect(compound3.dataItems.dataHeader.value(2)).toBe('PUBCHEM_CONFORMER_DIVERSEORDER');
expect(compound3.dataItems.dataHeader.value(2)).toBe('<PUBCHEM_CONFORMER_DIVERSEORDER>');
expect(compound3.dataItems.data.value(2)).toBe('1\n11\n10\n3\n15\n17\n13\n5\n16\n7\n14\n9\n8\n4\n18\n6\n12\n2');
expect(compound3.dataItems.dataHeader.value(21)).toBe('PUBCHEM_COORDINATE_TYPE');
expect(compound3.dataItems.dataHeader.value(21)).toBe('<PUBCHEM_COORDINATE_TYPE>');
expect(compound3.dataItems.data.value(21)).toBe('2\n5\n10');
});
});

View File

@@ -13,16 +13,20 @@ import { Tokenizer, TokenBuilder } from '../common/text/tokenizer';
import { TokenColumnProvider as TokenColumn } from '../common/text/column/token';
/** http://c4.cabrillo.edu/404/ctfile.pdf - page 41 */
export interface SdfFile {
readonly compounds: {
readonly molFile: MolFile,
readonly dataItems: {
readonly dataHeader: Column<string>,
readonly data: Column<string>
}
}[]
export interface SdfFileCompound {
readonly molFile: MolFile,
readonly dataItems: {
readonly dataHeader: Column<string>,
readonly data: Column<string>
}
}
export interface SdfFile {
readonly compounds: SdfFileCompound[]
}
const delimiter = '$$$$';
function handleDataItems(tokenizer: Tokenizer): { dataHeader: Column<string>, data: Column<string> } {
const dataHeader = TokenBuilder.create(tokenizer.data, 32);
@@ -33,8 +37,8 @@ function handleDataItems(tokenizer: Tokenizer): { dataHeader: Column<string>, da
if (line.startsWith(delimiter)) break;
if (!line) continue;
if (line.startsWith('> <')) {
TokenBuilder.add(dataHeader, tokenizer.tokenStart + 3, tokenizer.tokenEnd - 1);
if (line.startsWith('> ')) {
TokenBuilder.add(dataHeader, tokenizer.tokenStart + 2, tokenizer.tokenEnd);
Tokenizer.markLine(tokenizer);
const start = tokenizer.tokenStart;
@@ -42,7 +46,7 @@ function handleDataItems(tokenizer: Tokenizer): { dataHeader: Column<string>, da
let added = false;
while (tokenizer.position < tokenizer.length) {
const line2 = Tokenizer.readLine(tokenizer);
if (!line2 || line2.startsWith(delimiter) || line2.startsWith('> <')) {
if (!line2 || line2.startsWith(delimiter) || line2.startsWith('> ')) {
TokenBuilder.add(data, start, end);
added = true;
break;

View File

@@ -3,7 +3,10 @@ import { CifWriter } from '../cif';
import { decodeMsgPack } from '../../common/msgpack/decode';
import { EncodedFile, EncodedCategory } from '../../common/binary-cif';
import { Field } from '../../reader/cif/binary/field';
import { TextEncoder } from '../cif/encoder/text';
import * as C from '../cif/encoder';
import { Column, Database, Table } from '../../../mol-data/db';
import { parseCifText } from '../../reader/cif/text/parser';
const cartn_x = Data.CifField.ofNumbers([1.001, 1.002, 1.003, 1.004, 1.005, 1.006, 1.007, 1.008, 1.009]);
const cartn_y = Data.CifField.ofNumbers([-3.0, -2.666, -2.3333, -2.0, -1.666, -1.333, -1.0, -0.666, -0.333]);
@@ -38,6 +41,30 @@ const encoding_aware_encoder = CifWriter.createEncoder({
])
});
test('cif writer value escaping', async () => {
const values = ['1', ' ', ' ', ` ' `, `a'`, `b"`, `"`, ' a ', `"'"`, `'"`, `\na`];
const table = Table.ofArrays({ values: Column.Schema.str }, { values });
const encoder = new TextEncoder();
C.Encoder.writeDatabase(encoder, 'test', Database.ofTables('test', { test: table._schema }, { test: table }));
encoder.encode();
const data = encoder.getData();
const result = await parseCifText(data).run();
if (result.isError) {
expect(false).toBe(true);
return;
}
const cat = result.result.blocks[0].categories['test'];
const parsed = cat.getField('values')?.toStringArray();
expect(values.length).toBe(parsed?.length);
for (let i = 0; i < values.length; i++) {
expect(values[i]).toBe(parsed?.[i]);
}
});
describe('encoding-config', () => {
const decoded = process(encoding_aware_encoder);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2017-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* Adapted from CIFTools.js (https://github.com/dsehnal/CIFTools.js)
*
@@ -208,55 +208,61 @@ function writeChecked(builder: StringBuilder, val: string) {
return false;
}
let escape = val.charCodeAt(0) === 95 /* _ */, escapeCharStart = '\'', escapeCharEnd = '\' ';
let hasWhitespace = false;
let hasSingle = false;
let hasDouble = false;
for (let i = 0, _l = val.length - 1; i < _l; i++) {
const fst = val.charCodeAt(0);
let escape = false;
let escapeKind = 0; // 0 => ', 1 => "
let hasSingleQuote = false, hasDoubleQuote = false;
for (let i = 0, _l = val.length - 1; i <= _l; i++) {
const c = val.charCodeAt(i);
switch (c) {
case 9: hasWhitespace = true; break; // \t
case 9: // \t
escape = true;
break;
case 10: // \n
writeMultiline(builder, val);
return true;
case 32: hasWhitespace = true; break; // ' '
case 32: // ' '
escape = true;
break;
case 34: // "
if (hasSingle) {
// no need to escape quote if it's the last char and the length is > 1
if (i && i === _l) break;
if (hasSingleQuote) {
// the string already has a " => use multiline value
writeMultiline(builder, val);
return true;
}
hasDouble = true;
hasDoubleQuote = true;
escape = true;
escapeCharStart = '\'';
escapeCharEnd = '\' ';
escapeKind = 0;
break;
case 39: // '
if (hasDouble) {
// no need to escape quote if it's the last char and the length is > 1
if (i && i === _l) break;
if (hasDoubleQuote) {
writeMultiline(builder, val);
return true;
}
hasSingleQuote = true;
escape = true;
hasSingle = true;
escapeCharStart = '"';
escapeCharEnd = '" ';
escapeKind = 1;
break;
}
}
const fst = val.charCodeAt(0);
if (!escape && (fst === 35 /* # */|| fst === 36 /* $ */ || fst === 59 /* ; */ || fst === 91 /* [ */ || fst === 93 /* ] */ || hasWhitespace)) {
escapeCharStart = '\'';
escapeCharEnd = '\' ';
if (!escape && (fst === 35 /* # */ || fst === 36 /* $ */ || fst === 59 /* ; */ || fst === 91 /* [ */ || fst === 93 /* ] */ || fst === 95 /* _ */)) {
escape = true;
}
if (escape) {
StringBuilder.writeSafe(builder, escapeCharStart);
StringBuilder.writeSafe(builder, escapeKind ? '"' : '\'');
StringBuilder.writeSafe(builder, val);
StringBuilder.writeSafe(builder, escapeCharEnd);
StringBuilder.writeSafe(builder, escapeKind ? '" ' : '\' ');
} else {
StringBuilder.writeSafe(builder, val);
StringBuilder.writeSafe(builder, ' ');

View File

@@ -474,7 +474,9 @@ namespace Vec3 {
/** Computes the angle between 2 vectors, reports in radians. */
export function angle(a: Vec3, b: Vec3) {
const theta = dot(a, b) / Math.sqrt(squaredMagnitude(a) * squaredMagnitude(b));
const denominator = Math.sqrt(squaredMagnitude(a) * squaredMagnitude(b));
if (denominator === 0) return Math.PI / 2;
const theta = dot(a, b) / denominator;
return Math.acos(clamp(theta, -1, 1)); // clamp to avoid numerical problems
}

View File

@@ -13,21 +13,26 @@ import { mmCIF_chemComp_schema } from '../../../mol-io/reader/cif/schema/mmcif-e
type Component = Table.Row<Pick<mmCIF_chemComp_schema, 'id' | 'name' | 'type'>>
const ProteinAtomIdsList = [
new Set([ 'CA' ]),
new Set([ 'C' ]),
new Set([ 'N' ])
new Set(['CA']),
new Set(['C']),
new Set(['N'])
];
const RnaAtomIdsList = [
new Set([ 'P', 'O3\'', 'O3*' ]),
new Set([ 'C4\'', 'C4*' ]),
new Set([ 'O2\'', 'O2*', 'F2\'', 'F2*' ])
new Set(['P', 'O3\'', 'O3*']),
new Set(['C4\'', 'C4*']),
new Set(['O2\'', 'O2*', 'F2\'', 'F2*'])
];
const DnaAtomIdsList = [
new Set([ 'P', 'O3\'', 'O3*' ]),
new Set([ 'C3\'', 'C3*' ]),
new Set([ 'O2\'', 'O2*', 'F2\'', 'F2*' ])
new Set(['P', 'O3\'', 'O3*']),
new Set(['C3\'', 'C3*']),
new Set(['O2\'', 'O2*', 'F2\'', 'F2*'])
];
/** Used to reduce false positives for atom name-based type guessing */
const NonPolymerNames = new Set([
'FMN', 'NCN', 'FNS', 'FMA' // Mononucleotides
]);
const StandardComponents = (function() {
const map = new Map<string, Component>();
const components: Component[] = [
@@ -151,9 +156,11 @@ export class ComponentBuilder {
this.set(StandardComponents.get(compId)!);
} else if (WaterNames.has(compId)) {
this.set({ id: compId, name: 'WATER', type: 'non-polymer' });
} else if (NonPolymerNames.has(compId)) {
this.set({ id: compId, name: this.namesMap.get(compId) || compId, type: 'non-polymer' });
} else {
const atomIds = this.getAtomIds(index);
if (CharmmIonComponents.has(compId) && atomIds.size === 1) {
if (atomIds.size === 1 && CharmmIonComponents.has(compId)) {
this.set(CharmmIonComponents.get(compId)!);
} else {
const type = this.getType(atomIds);

View File

@@ -11,6 +11,7 @@ import { Vec3 } from '../../mol-math/linear-algebra';
import { degToRad, halfPI } from '../../mol-math/misc';
import { Cell } from '../../mol-math/geometry/spacegroup/cell';
import { Mutable } from '../../mol-util/type-helpers';
import { EPSILON, equalEps } from '../../mol-math/linear-algebra/3d/common';
const charmmTimeUnitFactor = 20.45482949774598;
@@ -38,6 +39,8 @@ export function coordinatesFromDcd(dcdFile: DcdFile): Task<Coordinates> {
x: dcdFrame.x,
y: dcdFrame.y,
z: dcdFrame.z,
xyzOrdering: { isIdentity: true }
};
if (dcdFrame.cell) {
@@ -64,7 +67,12 @@ export function coordinatesFromDcd(dcdFile: DcdFile): Task<Coordinates> {
} else {
frame.cell = Cell.create(
Vec3.create(c[0], c[2], c[5]),
Vec3.create(degToRad(c[1]), degToRad(c[3]), degToRad(c[4]))
// interpret angles very close to 0 as 90 deg
Vec3.create(
degToRad(equalEps(c[1], 0, EPSILON) ? 90 : c[1]),
degToRad(equalEps(c[3], 0, EPSILON) ? 90 : c[3]),
degToRad(equalEps(c[4], 0, EPSILON) ? 90 : c[4])
)
);
}
}

View File

@@ -17,7 +17,7 @@ import { ModelFormat } from '../format';
import { IndexPairBonds } from './property/bonds/index-pair';
import { Trajectory } from '../../mol-model/structure';
async function getModels(mol: MolFile, ctx: RuntimeContext) {
export async function getMolModels(mol: MolFile, format: ModelFormat<any> | undefined, ctx: RuntimeContext) {
const { atoms, bonds } = mol;
const MOL = Column.ofConst('MOL', mol.atoms.count, Column.Schema.str);
@@ -61,7 +61,7 @@ async function getModels(mol: MolFile, ctx: RuntimeContext) {
atom_site
});
const models = await createModels(basics, MolFormat.create(mol), ctx);
const models = await createModels(basics, format ?? MolFormat.create(mol), ctx);
if (models.frameCount > 0) {
const indexA = Column.ofIntArray(Column.mapToArray(bonds.atomIdxA, x => x - 1, Int32Array));
@@ -91,5 +91,5 @@ namespace MolFormat {
}
export function trajectoryFromMol(mol: MolFile): Task<Trajectory> {
return Task.create('Parse MOL', ctx => getModels(mol, ctx));
return Task.create('Parse MOL', ctx => getMolModels(mol, void 0, ctx));
}

View File

@@ -6,7 +6,7 @@
import { Column, Table } from '../../mol-data/db';
import { Model } from '../../mol-model/structure/model';
import { MoleculeType } from '../../mol-model/structure/model/types';
import { BondType, MoleculeType } from '../../mol-model/structure/model/types';
import { RuntimeContext, Task } from '../../mol-task';
import { createModels } from './basic/parser';
import { BasicSchema, createBasic } from './basic/schema';
@@ -74,8 +74,33 @@ async function getModels(mol2: Mol2File, ctx: RuntimeContext) {
if (_models.frameCount > 0) {
const indexA = Column.ofIntArray(Column.mapToArray(bonds.origin_atom_id, x => x - 1, Int32Array));
const indexB = Column.ofIntArray(Column.mapToArray(bonds.target_atom_id, x => x - 1, Int32Array));
const order = Column.ofIntArray(Column.mapToArray(bonds.bond_type, x => x === 'ar' ? 1 : parseInt(x), Int8Array));
const pairBonds = IndexPairBonds.fromData({ pairs: { indexA, indexB, order }, count: atoms.count });
const order = Column.ofIntArray(Column.mapToArray(bonds.bond_type, x => {
switch (x) {
case 'ar': // aromatic
case 'am': // amide
case 'un': // unknown
return 1;
case 'du': // dummy
case 'nc': // not connected
return 0;
default:
return parseInt(x);
}
}, Int8Array));
const flag = Column.ofIntArray(Column.mapToArray(bonds.bond_type, x => {
switch (x) {
case 'ar': // aromatic
return BondType.Flag.Aromatic | BondType.Flag.Covalent;
case 'du': // dummy
case 'nc': // not connected
return BondType.Flag.None;
case 'am': // amide
case 'un': // unknown
default:
return BondType.Flag.Covalent;
}
}, Int8Array));
const pairBonds = IndexPairBonds.fromData({ pairs: { indexA, indexB, order, flag }, count: atoms.count });
const first = _models.representative;
IndexPairBonds.Provider.set(first, pairBonds);

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { SdfFileCompound } from '../../mol-io/reader/sdf/parser';
import { Trajectory } from '../../mol-model/structure';
import { Task } from '../../mol-task';
import { ModelFormat } from '../format';
import { getMolModels } from './mol';
export { SdfFormat };
type SdfFormat = ModelFormat<SdfFileCompound>
namespace SdfFormat {
export function is(x?: ModelFormat): x is SdfFormat {
return x?.kind === 'sdf';
}
export function create(mol: SdfFileCompound): SdfFormat {
return { kind: 'sdf', name: mol.molFile.title, data: mol };
}
}
export function trajectoryFromSdf(mol: SdfFileCompound): Task<Trajectory> {
return Task.create('Parse SDF', ctx => getMolModels(mol.molFile, SdfFormat.create(mol), ctx));
}

View File

@@ -61,6 +61,14 @@ export function guessElementSymbolString(str: string) {
) return str;
}
if (l === 3) { // three chars
if (str === 'SOD') return 'NA';
if (str === 'POT') return 'K';
if (str === 'CES') return 'CS';
if (str === 'CAL') return 'CA';
if (str === 'CLA') return 'CL';
}
const c = str[0];
if (c === 'C' || c === 'H' || c === 'N' || c === 'O' || c === 'P' || c === 'S') return c;

View File

@@ -30,6 +30,7 @@ export function coordinatesFromXtc(file: XtcFile): Task<Coordinates> {
x: file.frames[i].x,
y: file.frames[i].y,
z: file.frames[i].z,
xyzOrdering: { isIdentity: true },
time: Time(offsetTime.value + deltaTime.value * i, deltaTime.unit)
});
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Model } from '../../mol-model/structure';
import { CustomPropertyDescriptor } from '../../mol-model/custom-property';
import { CustomModelProperty } from '../common/custom-model-property';
import { calcHelixOrientation, HelixOrientation } from './helix-orientation/helix-orientation';
export const HelixOrientationParams = { };
export type HelixOrientationParams = typeof HelixOrientationParams
export type HelixOrientationProps = PD.Values<HelixOrientationParams>
export type HelixOrientationValue = HelixOrientation;
export const HelixOrientationProvider: CustomModelProperty.Provider<HelixOrientationParams, HelixOrientationValue> = CustomModelProperty.createProvider({
label: 'Helix Orientation',
descriptor: CustomPropertyDescriptor({
name: 'molstar_helix_orientation'
}),
type: 'dynamic',
defaultParams: {},
getParams: () => ({}),
isApplicable: (data: Model) => true,
obtain: async (ctx, data) => {
return { value: calcHelixOrientation(data) };
}
});

View File

@@ -0,0 +1,138 @@
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { ElementIndex } from '../../../mol-model/structure';
import { Segmentation } from '../../../mol-data/int/segmentation';
import { SortedRanges } from '../../../mol-data/int/sorted-ranges';
import { OrderedSet } from '../../../mol-data/int';
import { Model } from '../../../mol-model/structure/model';
import { Vec3 } from '../../../mol-math/linear-algebra';
export interface HelixOrientation {
centers: ArrayLike<number>
}
/** Usees same definition as GROMACS' helixorient */
export function calcHelixOrientation(model: Model): HelixOrientation {
const { x, y, z } = model.atomicConformation;
const { polymerType, traceElementIndex } = model.atomicHierarchy.derived.residue;
const n = polymerType.length;
const elements = OrderedSet.ofBounds(0, model.atomicConformation.atomId.rowCount) as OrderedSet<ElementIndex>;
const polymerIt = SortedRanges.transientSegments(model.atomicRanges.polymerRanges, elements);
const residueIt = Segmentation.transientSegments(model.atomicHierarchy.residueAtomSegments, elements);
const centers = new Float32Array(n * 3);
const axes = new Float32Array(n * 3);
let i = 0;
let j = -1;
let s = -1;
const a1 = Vec3();
const a2 = Vec3();
const a3 = Vec3();
const a4 = Vec3();
const r12 = Vec3();
const r23 = Vec3();
const r34 = Vec3();
const v1 = Vec3();
const v2 = Vec3();
const vt = Vec3();
const diff13 = Vec3();
const diff24 = Vec3();
const axis = Vec3();
const prevAxis = Vec3();
while (polymerIt.hasNext) {
const ps = polymerIt.move();
residueIt.setSegment(ps);
i = -1;
s = -1;
while (residueIt.hasNext) {
i += 1;
const { index } = residueIt.move();
if (i === 0) s = index;
j = (index - 2);
const j3 = j * 3;
Vec3.copy(a1, a2);
Vec3.copy(a2, a3);
Vec3.copy(a3, a4);
const eI = traceElementIndex[index];
Vec3.set(a4, x[eI], y[eI], z[eI]);
if (i < 3) continue;
Vec3.sub(r12, a2, a1);
Vec3.sub(r23, a3, a2);
Vec3.sub(r34, a4, a3);
Vec3.sub(diff13, r12, r23);
Vec3.sub(diff24, r23, r34);
Vec3.cross(axis, diff13, diff24);
Vec3.normalize(axis, axis);
Vec3.toArray(axis, axes, j3);
const tmp = Math.cos(Vec3.angle(diff13, diff24));
const diff13Length = Vec3.magnitude(diff13);
const diff24Length = Vec3.magnitude(diff24);
const r = (
Math.sqrt(diff24Length * diff13Length) /
// clamp, to avoid numerical instabilities for when
// angle between diff13 and diff24 is close to 0
Math.max(2.0, 2.0 * (1.0 - tmp))
);
Vec3.scale(v1, diff13, r / diff13Length);
Vec3.sub(v1, a2, v1);
Vec3.toArray(v1, centers, j3);
Vec3.scale(v2, diff24, r / diff24Length);
Vec3.sub(v2, a3, v2);
Vec3.toArray(v2, centers, j3 + 3);
Vec3.copy(prevAxis, axis);
}
// calc axis as dir of second and third center pos
// project first trace atom onto axis to get first center pos
const s3 = s * 3;
Vec3.fromArray(v1, centers, s3 + 3);
Vec3.fromArray(v2, centers, s3 + 6);
Vec3.normalize(axis, Vec3.sub(axis, v1, v2));
const sI = traceElementIndex[s];
Vec3.set(a1, x[sI], y[sI], z[sI]);
Vec3.copy(vt, a1);
Vec3.projectPointOnVector(vt, vt, axis, v1);
Vec3.toArray(vt, centers, s3);
// calc axis as dir of n-1 and n-2 center pos
// project last traceAtom onto axis to get last center pos
const e = j + 2;
const e3 = e * 3;
Vec3.fromArray(v1, centers, e3 - 3);
Vec3.fromArray(v2, centers, e3 - 6);
Vec3.normalize(axis, Vec3.sub(axis, v1, v2));
const eI = traceElementIndex[e];
Vec3.set(a1, x[eI], y[eI], z[eI]);Vec3.copy(vt, a1);
Vec3.projectPointOnVector(vt, vt, axis, v1);
Vec3.toArray(vt, centers, e3);
}
return {
centers
};
}

View File

@@ -83,6 +83,6 @@ export const AccessibleSurfaceAreaColorThemeProvider: ColorTheme.Provider<Access
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure,
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? AccessibleSurfaceAreaProvider.attach(ctx, data.structure, void 0, true) : Promise.resolve(),
detach: (data) => data.structure && data.structure.customPropertyDescriptors.reference(AccessibleSurfaceAreaProvider.descriptor, false)
detach: (data) => data.structure && AccessibleSurfaceAreaProvider.ref(data.structure, false)
}
};

View File

@@ -117,6 +117,6 @@ export const InteractionTypeColorThemeProvider: ColorTheme.Provider<InteractionT
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure,
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? InteractionsProvider.attach(ctx, data.structure, void 0, true) : Promise.resolve(),
detach: (data) => data.structure && data.structure.customPropertyDescriptors.reference(InteractionsProvider.descriptor, false)
detach: (data) => data.structure && InteractionsProvider.ref(data.structure, false)
}
};

View File

@@ -70,6 +70,6 @@ export const CrossLinkColorThemeProvider: ColorTheme.Provider<CrossLinkColorThem
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && CrossLinkRestraint.isApplicable(ctx.structure),
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? CrossLinkRestraintProvider.attach(ctx, data.structure, void 0, true) : Promise.resolve(),
detach: (data) => data.structure && data.structure.customPropertyDescriptors.reference(CrossLinkRestraintProvider.descriptor, false)
detach: (data) => data.structure && CrossLinkRestraintProvider.ref(data.structure, false)
}
};

View File

@@ -29,7 +29,7 @@ namespace BestDatabaseSequenceMapping {
type: 'static',
defaultParams: {},
getParams: () => ({}),
isApplicable: (data: Model) => MmcifFormat.is(data.sourceData) && data.sourceData.data.frame.categories?.atom_site.fieldNames.indexOf('db_name') >= 0,
isApplicable: (data: Model) => MmcifFormat.is(data.sourceData) && data.sourceData.data.frame.categories?.atom_site?.fieldNames.indexOf('db_name') >= 0,
obtain: async (ctx, data) => {
return { value: fromCif(data) };
}
@@ -92,4 +92,4 @@ namespace BestDatabaseSequenceMapping {
return { dbName, accession, num, residue };
}
}
}

View File

@@ -98,7 +98,7 @@ export const BestDatabaseSequenceMappingColorThemeProvider: ColorTheme.Provider<
detach: (data) => {
if (!data.structure) return;
for (const m of data.structure.models) {
m.customProperties.reference(BestDatabaseSequenceMapping.Provider.descriptor, false);
BestDatabaseSequenceMapping.Provider.ref(m, false);
}
}
}

View File

@@ -39,6 +39,18 @@ const ProteinOneLetterCodes: { [name: string]: AminoAlphabet } = {
'SEC': 'U', // as per IUPAC definition
'PYL': 'O', // as per IUPAC definition
// charmm ff
'HSD': 'H', 'HSE': 'H', 'HSP': 'H',
'LSN': 'K',
'ASPP': 'D',
'GLUP': 'E',
// amber ff
'HID': 'H', 'HIE': 'H', 'HIP': 'H',
'LYN': 'K',
'ASH': 'D',
'GLH': 'E',
};
const DnaOneLetterCodes: { [name: string]: NuclecicAlphabet } = {

View File

@@ -1,10 +1,11 @@
/**
* Copyright (c) 2019-2020 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>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { UUID } from '../../../mol-util';
import { arrayEqual, UUID } from '../../../mol-util';
import { Cell } from '../../../mol-math/geometry/spacegroup/cell';
import { AtomicConformation } from '../model/properties/atomic';
import { Column } from '../../../mol-data/db';
@@ -34,6 +35,12 @@ export interface Frame {
readonly fy: ArrayLike<number>
readonly fz: ArrayLike<number>
}
readonly xyzOrdering: {
isIdentity: boolean,
frozen?: boolean,
index?: ArrayLike<number>,
}
}
//
@@ -85,41 +92,106 @@ namespace Coordinates {
hasVelocities,
hasForces,
deltaTime,
timeOffset
timeOffset,
};
}
export function getAtomicConformation(frame: Frame, atomId: Column<number>): AtomicConformation {
/**
* Only use ordering if it's not identity.
*/
export function getAtomicConformation(frame: Frame, atomId: Column<number>, ordering?: ArrayLike<number>): AtomicConformation {
let { x, y, z } = frame;
if (frame.xyzOrdering.frozen) {
if (ordering) {
if (frame.xyzOrdering.isIdentity) {
// simple list reordering
x = getOrderedCoords(x, ordering);
y = getOrderedCoords(y, ordering);
z = getOrderedCoords(z, ordering);
} else if (!arrayEqual(frame.xyzOrdering.index! as any, ordering as any)) {
x = getSourceOrderedCoords(x, frame.xyzOrdering.index!, ordering);
y = getSourceOrderedCoords(y, frame.xyzOrdering.index!, ordering);
z = getSourceOrderedCoords(z, frame.xyzOrdering.index!, ordering);
}
} else if (!frame.xyzOrdering.isIdentity) {
x = getInvertedCoords(x, frame.xyzOrdering.index!);
y = getInvertedCoords(y, frame.xyzOrdering.index!);
z = getInvertedCoords(z, frame.xyzOrdering.index!);
}
} else if (ordering) {
if (frame.xyzOrdering.isIdentity) {
frame.xyzOrdering.isIdentity = false;
frame.xyzOrdering.index = ordering;
reorderCoordsInPlace(x as unknown as number[], ordering);
reorderCoordsInPlace(y as unknown as number[], ordering);
reorderCoordsInPlace(z as unknown as number[], ordering);
} else {
// is current ordering is not the same as requested?
// => copy the conformations into a new array
if (!arrayEqual(frame.xyzOrdering.index! as any, ordering as any)) {
x = getSourceOrderedCoords(x, frame.xyzOrdering.index!, ordering);
y = getSourceOrderedCoords(y, frame.xyzOrdering.index!, ordering);
z = getSourceOrderedCoords(z, frame.xyzOrdering.index!, ordering);
}
}
}
// once the conformation has been accessed at least once, freeze it.
// => any other request to the frame with different ordering will result in a copy.
frame.xyzOrdering.frozen = true;
return {
id: UUID.create22(),
atomId,
occupancy: Column.ofConst(1, frame.elementCount, Column.Schema.int),
B_iso_or_equiv: Column.ofConst(0, frame.elementCount, Column.Schema.float),
xyzDefined: true,
x: frame.x,
y: frame.y,
z: frame.z,
x,
y,
z,
};
}
function reorderCoords(xs: ArrayLike<number>, index: ArrayLike<number>) {
const ret = new Float32Array(xs.length);
const _reorderBuffer = [0.123];
function reorderCoordsInPlace(xs: number[], index: ArrayLike<number>) {
const buffer = _reorderBuffer;
for (let i = 0, _i = xs.length; i < _i; i++) {
ret[i] = xs[index[i]];
buffer[i] = xs[index[i]];
}
for (let i = 0, _i = xs.length; i < _i; i++) {
xs[i] = buffer[i];
}
}
function getSourceOrderedCoords(xs: ArrayLike<number>, srcIndex: ArrayLike<number>, index: ArrayLike<number>) {
const ret = new Float32Array(xs.length);
for (let i = 0, _i = xs.length; i < _i; i++) {
ret[i] = xs[srcIndex[index[i]]];
}
return ret;
}
export function getAtomicConformationReordered(frame: Frame, atomId: Column<number>, srcIndex: ArrayLike<number>): AtomicConformation {
return {
id: UUID.create22(),
atomId,
occupancy: Column.ofConst(1, frame.elementCount, Column.Schema.int),
B_iso_or_equiv: Column.ofConst(0, frame.elementCount, Column.Schema.float),
xyzDefined: true,
x: reorderCoords(frame.x, srcIndex),
y: reorderCoords(frame.y, srcIndex),
z: reorderCoords(frame.z, srcIndex)
};
function getOrderedCoords(xs: ArrayLike<number>, index: ArrayLike<number>) {
const ret = new Float32Array(xs.length);
for (let i = 0, _i = xs.length; i < _i; i++) {
ret[i] = xs[index[i]];
}
return ret;
}
function getInvertedCoords(xs: ArrayLike<number>, index: ArrayLike<number>) {
const ret = new Float32Array(xs.length);
for (let i = 0, _i = xs.length; i < _i; i++) {
ret[index[i]] = xs[i];
}
return ret;
}
}

View File

@@ -105,9 +105,7 @@ export namespace Model {
...model,
id: UUID.create22(),
modelNum: i,
atomicConformation: isIdentity
? Coordinates.getAtomicConformation(f, model.atomicConformation.atomId)
: Coordinates.getAtomicConformationReordered(f, model.atomicConformation.atomId, srcIndexArray!),
atomicConformation: Coordinates.getAtomicConformation(f, model.atomicConformation.atomId, srcIndexArray),
// TODO: add support for supplying sphere and gaussian coordinates in addition to atomic coordinates?
// coarseConformation: coarse.conformation,
customProperties: new CustomProperties(),

View File

@@ -1,33 +0,0 @@
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
const elm1 = [ 'H', 'C', 'O', 'N', 'S', 'P' ];
const elm2 = [ 'NA', 'CL', 'FE' ];
function charAtIsNumber(str: string, index: number) {
const code = str.charCodeAt(index);
return code >= 48 && code <= 57;
}
export function guessElement(str: string) {
let at = str.trim().toUpperCase();
if (charAtIsNumber(at, 0)) at = at.substr(1);
// parse again to check for a second integer
if (charAtIsNumber(at, 0)) at = at.substr(1);
const n = at.length;
if (n === 0) return '';
if (n === 1) return at;
if (n === 2) {
if (elm2.indexOf(at) !== -1) return at;
if (elm1.indexOf(at[0]) !== -1) return at[0];
}
if (n >= 3) {
if (elm1.indexOf(at[0]) !== -1) return at[0];
}
return '';
}

View File

@@ -99,7 +99,7 @@ export const PolymerTypeAtomRoleId: { [k in PolymerType]: { [k in AtomRole]: Set
[PolymerType.Protein]: {
trace: new Set(['CA']),
directionFrom: new Set(['C']),
directionTo: new Set(['O', 'OC1', 'O1', 'OX1', 'OXT']),
directionTo: new Set(['O', 'OC1', 'O1', 'OX1', 'OXT', 'OT1']),
backboneStart: new Set(['N']),
backboneEnd: new Set(['C']),
// CA1 is used e.g. in GFP chromophores
@@ -246,8 +246,13 @@ export const AminoAcidNamesL = new Set([
'HIS', 'ARG', 'LYS', 'ILE', 'PHE', 'LEU', 'TRP', 'ALA', 'MET', 'PRO', 'CYS',
'ASN', 'VAL', 'GLY', 'SER', 'GLN', 'TYR', 'ASP', 'GLU', 'THR', 'SEC', 'PYL',
'UNK', // unknown amino acid from CCD
'MSE', 'SEP', 'TPO', 'PTR', 'PCA' // common
'MSE', 'SEP', 'TPO', 'PTR', 'PCA', // common from CCD
// charmm ff
'HSD', 'HSE', 'HSP', 'LSN', 'ASPP', 'GLUP',
// amber ff
'HID', 'HIE', 'HIP', 'LYN', 'ASH', 'GLH',
]);
export const AminoAcidNamesD = new Set([
'DAL', // D-ALANINE
@@ -586,7 +591,13 @@ export const MaxAsa = {
'THR': 163.0,
'TRP': 264.0,
'TYR': 255.0,
'VAL': 165.0
'VAL': 165.0,
// charmm ff
'HSD': 216.0, 'HSE': 216.0, 'HSP': 216.0,
// amber ff
'HID': 216.0, 'HIE': 216.0, 'HIP': 216.0, 'ASH': 187.0, 'GLH': 214.0,
};
export const DefaultMaxAsa = 121.0;
@@ -620,9 +631,9 @@ export namespace BondType {
'covalent': Flag.Covalent,
'metal-coordination': Flag.MetallicCoordination,
'hydrogen-bond': Flag.HydrogenBond,
'disulfide': Flag.HydrogenBond,
'aromatic': Flag.HydrogenBond,
'computed': Flag.HydrogenBond,
'disulfide': Flag.Disulfide,
'aromatic': Flag.Aromatic,
'computed': Flag.Computed,
};
export type Names = keyof typeof Names
@@ -680,6 +691,12 @@ export const ResidueHydrophobicity = {
'THR': [0.14, 0.25, 0.11],
'TRP': [-1.85, -2.09, -0.24],
'TYR': [-0.94, -0.71, 0.23],
'VAL': [0.07, -0.46, -0.53]
'VAL': [0.07, -0.46, -0.53],
// charmm ff
'HSD': [0.17, 0.11, -0.06], 'HSE': [0.17, 0.11, -0.06], 'HSP': [0.96, 2.33, 1.37],
// amber ff
'HID': [0.17, 0.11, -0.06], 'HIE': [0.17, 0.11, -0.06], 'HIP': [0.96, 2.33, 1.37],
};
export const DefaultResidueHydrophobicity = [0.00, 0.00, 0.00];

View File

@@ -278,7 +278,7 @@ function checkConnected(ctx: IsConnectedToCtx, structure: Structure) {
for (let li = 0; li < buCount; li++) {
const lu = bondedUnits[li];
const bUnit = structure.unitMap.get(lu.unitB) as Unit.Atomic;
const bUnit = input.unitMap.get(lu.unitB) as Unit.Atomic;
const bElements = bUnit.elements;
const bonds = lu.getEdges(inputIndex);
for (let bi = 0, _bi = bonds.length; bi < _bi; bi++) {

View File

@@ -368,13 +368,16 @@ class Structure {
private _child: Structure | undefined;
private _target: Structure | undefined;
private _proxy: Structure | undefined;
/**
* For `structure` with `parent` this returns a proxy that
* targets `parent` and has `structure` attached as a child.
*/
asParent(): Structure {
return this.parent ? new Structure(this.parent.units, this.parent.unitMap, this.parent.unitIndexMap, this.parent.state, { child: this, target: this.parent }) : this;
if (this._proxy) return this._proxy;
this._proxy = this.parent ? new Structure(this.parent.units, this.parent.unitMap, this.parent.unitIndexMap, this.parent.state, { child: this, target: this.parent }) : this;
return this._proxy;
}
get child(): Structure | undefined {

View File

@@ -117,7 +117,6 @@ namespace UnitRing {
// comes e.g. from `chem_comp_bond.pdbx_aromatic_flag`
if (BondType.is(BondType.Flag.Aromatic, flags[j])) {
if (SortedArray.has(ring, b[j])) aromaticBondCount += 1;
}
}
}

View File

@@ -191,6 +191,9 @@ export const CubeProvider = DataFormatProvider({
}
});
type DsCifParams = { entryId?: string | string[] };
export const DscifProvider = DataFormatProvider({
label: 'DensityServer CIF',
description: 'DensityServer CIF',
@@ -200,7 +203,7 @@ export const DscifProvider = DataFormatProvider({
isApplicable: (info, data) => {
return guessCifVariant(info, data) === 'dscif' ? true : false;
},
parse: async (plugin, data, params?: Params) => {
parse: async (plugin, data, params?: DsCifParams) => {
const cifCell = await plugin.build().to(data).apply(StateTransforms.Data.ParseCif).commit();
const b = plugin.build().to(cifCell);
const blocks = cifCell.obj!.data.blocks.slice(1); // zero block contains query meta-data
@@ -208,8 +211,11 @@ export const DscifProvider = DataFormatProvider({
if (blocks.length !== 1 && blocks.length !== 2) throw new Error('unknown number of blocks');
const volumes: StateObjectSelector<PluginStateObject.Volume.Data>[] = [];
let i = 0;
for (const block of blocks) {
volumes.push(b.apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: block.header, entryId: params?.entryId }).selector);
const entryId = Array.isArray(params?.entryId) ? params?.entryId[i] : params?.entryId;
volumes.push(b.apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: block.header, entryId }).selector);
i++;
}
await b.commit();

View File

@@ -125,12 +125,13 @@ export namespace PluginStateObject {
url: string | Asset.Url,
isBinary: boolean,
format: string,
entryId?: string,
entryId?: string | string[],
isovalues: {
type: 'absolute' | 'relative',
value: number,
color: Color,
alpha?: number
alpha?: number,
volumeIndex?: number,
}[]
}

View File

@@ -455,18 +455,20 @@ const LazyVolume = PluginStateTransform.BuiltIn({
url: PD.Url(''),
isBinary: PD.Boolean(false),
format: PD.Text('ccp4'), // TODO: use Select based on available formats
entryId: PD.Text(''),
entryId: PD.Value<string | string[]>('', { isHidden: true }),
isovalues: PD.ObjectList({
type: PD.Text<'absolute' | 'relative'>('relative'), // TODO: Select
value: PD.Numeric(0),
color: PD.Color(ColorNames.black),
alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 })
alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
volumeIndex: PD.Numeric(0),
}, e => `${e.type} ${e.value}`)
}
})({
apply({ a, params }) {
return Task.create('Lazy Volume', async ctx => {
return new SO.Volume.Lazy(params, { label: `${params.entryId || params.url}`, description: 'Lazy Volume' });
const entryId = Array.isArray(params.entryId) ? params.entryId.join(', ') : params.entryId;
return new SO.Volume.Lazy(params, { label: `${entryId || params.url}`, description: 'Lazy Volume' });
});
}
});

View File

@@ -40,6 +40,7 @@ import { coordinatesFromXtc } from '../../mol-model-formats/structure/xtc';
import { parseXyz } from '../../mol-io/reader/xyz/parser';
import { trajectoryFromXyz } from '../../mol-model-formats/structure/xyz';
import { parseSdf } from '../../mol-io/reader/sdf/parser';
import { trajectoryFromSdf } from '../../mol-model-formats/structure/sdf';
export { CoordinatesFromDcd };
export { CoordinatesFromXtc };
@@ -308,8 +309,8 @@ const TrajectoryFromSDF = PluginStateTransform.BuiltIn({
const models: Model[] = [];
for (const { molFile } of parsed.result.compounds) {
const traj = await trajectoryFromMol(molFile).runInContext(ctx);
for (const compound of parsed.result.compounds) {
const traj = await trajectoryFromSdf(compound).runInContext(ctx);
for (let i = 0; i < traj.frameCount; i++) {
models.push(await Task.resolveInContext(traj.getFrameAtIndex(i), ctx));
}

View File

@@ -41,6 +41,9 @@ export function MoleculeSvg() { return _Molecule; }
const _CubeOutline = <svg width='24px' height='24px' viewBox='0 0 24 24' strokeWidth='0.1px'><path d="M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L6.04,7.5L12,10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z" /></svg>;
export function CubeOutlineSvg() { return _CubeOutline; }
const _CubeScan = <svg width='24px' height='24px' viewBox='0 0 24 24' strokeWidth='0.1px'><path d="M17,22V20H20V17H22V20.5C22,20.89 21.84,21.24 21.54,21.54C21.24,21.84 20.89,22 20.5,22H17M7,22H3.5C3.11,22 2.76,21.84 2.46,21.54C2.16,21.24 2,20.89 2,20.5V17H4V20H7V22M17,2H20.5C20.89,2 21.24,2.16 21.54,2.46C21.84,2.76 22,3.11 22,3.5V7H20V4H17V2M7,2V4H4V7H2V3.5C2,3.11 2.16,2.76 2.46,2.46C2.76,2.16 3.11,2 3.5,2H7M13,17.25L17,14.95V10.36L13,12.66V17.25M12,10.92L16,8.63L12,6.28L8,8.63L12,10.92M7,14.95L11,17.25V12.66L7,10.36V14.95M18.23,7.59C18.73,7.91 19,8.34 19,8.91V15.23C19,15.8 18.73,16.23 18.23,16.55L12.75,19.73C12.25,20.05 11.75,20.05 11.25,19.73L5.77,16.55C5.27,16.23 5,15.8 5,15.23V8.91C5,8.34 5.27,7.91 5.77,7.59L11.25,4.41C11.5,4.28 11.75,4.22 12,4.22C12.25,4.22 12.5,4.28 12.75,4.41L18.23,7.59Z" /></svg>;
export function CubeScanSvg() { return _CubeScan; }
const _CubeSend = <svg width='24px' height='24px' viewBox='0 0 24 24' strokeWidth='0.1px'><path d="M16,4L9,8.04V15.96L16,20L23,15.96V8.04M16,6.31L19.8,8.5L16,10.69L12.21,8.5M0,7V9H7V7M11,10.11L15,12.42V17.11L11,14.81M21,10.11V14.81L17,17.11V12.42M2,11V13H7V11M4,15V17H7V15" /></svg>;
export function CubeSendSvg() { return _CubeSend; }

View File

@@ -205,19 +205,21 @@ export class VolumeSourceControls extends CollapsableControls<{}, VolumeSourceCo
try {
const plugin = this.plugin;
await plugin.dataTransaction(async () => {
const data = await plugin.builders.data.download({ url, isBinary, label: entryId }, { state: { isGhost: true } });
const data = await plugin.builders.data.download({ url, isBinary }, { state: { isGhost: true } });
const parsed = await plugin.dataFormats.get(format)!.parse(plugin, data, { entryId });
const volume = (parsed.volume || parsed.volumes[0]) as StateObjectSelector<PluginStateObject.Volume.Data>;
if (!volume?.isOk) throw new Error('Failed to parse any volume.');
const firstVolume = (parsed.volume || parsed.volumes[0]) as StateObjectSelector<PluginStateObject.Volume.Data>;
if (!firstVolume?.isOk) throw new Error('Failed to parse any volume.');
const repr = plugin.build().to(volume);
const repr = plugin.build();
for (const iso of isovalues) {
repr.apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.plugin, volume.data!, {
type: 'isosurface',
typeParams: { alpha: iso.alpha ?? 1, isoValue: iso.type === 'absolute' ? { kind: 'absolute', absoluteValue: iso.value } : { kind: 'relative', relativeValue: iso.value } },
color: 'uniform',
colorParams: { value: iso.color }
}));
repr
.to(parsed.volumes?.[iso.volumeIndex ?? 0] ?? parsed.volume)
.apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.plugin, firstVolume.data!, {
type: 'isosurface',
typeParams: { alpha: iso.alpha ?? 1, isoValue: iso.type === 'absolute' ? { kind: 'absolute', absoluteValue: iso.value } : { kind: 'relative', relativeValue: iso.value } },
color: 'uniform',
colorParams: { value: iso.color }
}));
}
await repr.commit();

View File

@@ -276,10 +276,12 @@ const VolumeStreamingVisual = PluginStateTransform.BuiltIn({
// TODO: is this correct behavior?
if (!channel) return StateTransformer.UpdateResult.Unchanged;
const visible = b.data.repr.state.visible;
const params = createVolumeProps(a.data, newParams.channel);
const props = { ...b.data.repr.props, ...params.type.params };
b.data.repr.setTheme(Theme.create(plugin.representation.volume.themes, { volume: channel.data }, params));
await b.data.repr.createOrUpdate(props, channel.data).runInContext(ctx);
b.data.repr.setState({ visible });
b.data.sourceData = channel.data;
// TODO: set the transform here as well in case the structure moves?

View File

@@ -118,6 +118,11 @@ export function ComplexVisual<G extends Geometry, P extends StructureParams & Ge
updateState.createGeometry = true;
}
if (currentStructure.child !== newStructure.child) {
// console.log('new child');
updateState.createGeometry = true;
}
if (updateState.updateSize && !('uSize' in renderObject.values)) {
updateState.createGeometry = true;
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -21,6 +21,7 @@ import { PuttyRepresentationProvider } from './representation/putty';
import { SpacefillRepresentationProvider } from './representation/spacefill';
import { LineRepresentationProvider } from './representation/line';
import { GaussianVolumeRepresentationProvider } from './representation/gaussian-volume';
import { BackboneRepresentationProvider } from './representation/backbone';
export class StructureRepresentationRegistry extends RepresentationRegistry<Structure, StructureRepresentationState> {
constructor() {
@@ -35,6 +36,7 @@ export class StructureRepresentationRegistry extends RepresentationRegistry<Stru
export namespace StructureRepresentationRegistry {
export const BuiltIn = {
'cartoon': CartoonRepresentationProvider,
'backbone': BackboneRepresentationProvider,
'ball-and-stick': BallAndStickRepresentationProvider,
'carbohydrate': CarbohydrateRepresentationProvider,
'ellipsoid': EllipsoidRepresentationProvider,

View File

@@ -1,29 +1,57 @@
// /**
// * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
// *
// * @author Alexander Rose <alexander.rose@weirdbyte.de>
// */
/**
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
// import { PolymerBackboneVisual, PolymerBackboneParams } from '../visual/polymer-backbone-cylinder';
// import { ParamDefinition as PD } from 'mol-util/param-definition';
// import { UnitsRepresentation } from '../units-representation';
// import { StructureRepresentation } from '../representation';
// import { Representation } from 'mol-repr/representation';
// import { ThemeRegistryContext } from 'mol-theme/theme';
// import { Structure } from 'mol-model/structure';
import { PolymerBackboneCylinderVisual, PolymerBackboneCylinderParams } from '../visual/polymer-backbone-cylinder';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { UnitsRepresentation } from '../units-representation';
import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder } from '../representation';
import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../../mol-repr/representation';
import { ThemeRegistryContext } from '../../../mol-theme/theme';
import { Structure } from '../../../mol-model/structure';
import { PolymerBackboneSphereParams, PolymerBackboneSphereVisual } from '../visual/polymer-backbone-sphere';
import { PolymerGapParams, PolymerGapVisual } from '../visual/polymer-gap-cylinder';
// export const BackboneParams = {
// ...PolymerBackboneParams,
// }
// export function getBackboneParams(ctx: ThemeRegistryContext, structure: Structure) {
// return BackboneParams // TODO return copy
// }
// export type BackboneProps = PD.DefaultValues<typeof BackboneParams>
const BackboneVisuals = {
'polymer-backbone-cylinder': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, PolymerBackboneCylinderParams>) => UnitsRepresentation('Polymer backbone cylinder', ctx, getParams, PolymerBackboneCylinderVisual),
'polymer-backbone-sphere': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, PolymerBackboneSphereParams>) => UnitsRepresentation('Polymer backbone sphere', ctx, getParams, PolymerBackboneSphereVisual),
'polymer-gap': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, PolymerGapParams>) => UnitsRepresentation('Polymer gap cylinder', ctx, getParams, PolymerGapVisual),
};
// export type BackboneRepresentation = StructureRepresentation<BackboneProps>
export const BackboneParams = {
...PolymerBackboneSphereParams,
...PolymerBackboneCylinderParams,
...PolymerGapParams,
sizeAspectRatio: PD.Numeric(1, { min: 0.1, max: 3, step: 0.1 }),
visuals: PD.MultiSelect(['polymer-backbone-cylinder', 'polymer-backbone-sphere', 'polymer-gap'], PD.objectToOptions(BackboneVisuals))
};
export type BackboneParams = typeof BackboneParams
export function getBackboneParams(ctx: ThemeRegistryContext, structure: Structure) {
const params = PD.clone(BackboneParams);
let hasGaps = false;
structure.units.forEach(u => {
if (!hasGaps && u.gapElements.length) hasGaps = true;
});
params.visuals.defaultValue = ['polymer-backbone-cylinder', 'polymer-backbone-sphere'];
if (hasGaps) params.visuals.defaultValue.push('polymer-gap');
return params;
}
// export function BackboneRepresentation(defaultProps: BackboneProps): BackboneRepresentation {
// return Representation.createMulti('Backbone', defaultProps, [
// UnitsRepresentation('Polymer backbone cylinder', defaultProps, PolymerBackboneVisual)
// ])
// }
export type BackboneRepresentation = StructureRepresentation<BackboneParams>
export function BackboneRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, BackboneParams>): BackboneRepresentation {
return Representation.createMulti('Backbone', ctx, getParams, StructureRepresentationStateBuilder, BackboneVisuals as unknown as Representation.Def<Structure, BackboneParams>);
}
export const BackboneRepresentationProvider = StructureRepresentationProvider({
name: 'backbone',
label: 'Backbone',
description: 'Displays polymer backbone with cylinders and spheres.',
factory: BackboneRepresentation,
getParams: getBackboneParams,
defaultValues: PD.getDefaultValues(BackboneParams),
defaultColorTheme: { name: 'chain-id' },
defaultSizeTheme: { name: 'uniform' },
isApplicable: (structure: Structure) => structure.polymerResidueCount > 0,
});

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -17,6 +17,7 @@ import { PolymerGapParams, PolymerGapVisual } from '../visual/polymer-gap-cylind
import { PolymerTraceParams, PolymerTraceVisual } from '../visual/polymer-trace-mesh';
import { SecondaryStructureProvider } from '../../../mol-model-props/computed/secondary-structure';
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { HelixOrientationProvider } from '../../../mol-model-props/computed/helix-orientation';
const CartoonVisuals = {
'polymer-trace': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, PolymerTraceParams>) => UnitsRepresentation('Polymer trace mesh', ctx, getParams, PolymerTraceVisual),
@@ -67,7 +68,17 @@ export const CartoonRepresentationProvider = StructureRepresentationProvider({
defaultSizeTheme: { name: 'uniform' },
isApplicable: (structure: Structure) => structure.polymerResidueCount > 0,
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, structure: Structure) => SecondaryStructureProvider.attach(ctx, structure, void 0, true),
detach: (data) => SecondaryStructureProvider.ref(data, false)
attach: async (ctx: CustomProperty.Context, structure: Structure) => {
await SecondaryStructureProvider.attach(ctx, structure, void 0, true);
for (const m of structure.models) {
await HelixOrientationProvider.attach(ctx, m, void 0, true);
}
},
detach: (data) => {
SecondaryStructureProvider.ref(data, false);
for (const m of data.models) {
HelixOrientationProvider.ref(m, false);
}
}
}
});

View File

@@ -66,7 +66,7 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
visuals.set(group.hashCode, { visual, group });
if (runtime.shouldUpdate) await runtime.update({ message: 'Creating or updating UnitsVisual', current: i, max: _groups.length });
}
} else if (structure && !Structure.areUnitIdsAndIndicesEqual(structure, _structure)) {
} else if (structure && (!Structure.areUnitIdsAndIndicesEqual(structure, _structure) || structure.child !== _structure.child)) {
// console.log(label, 'structures not equivalent');
// Tries to re-use existing visuals for the groups of the new structure.
// Creates additional visuals if needed, destroys left-over visuals.

View File

@@ -120,6 +120,11 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
updateState.updateColor = true;
}
if (currentStructureGroup.structure.child !== newStructureGroup.structure.child) {
// console.log('new child');
updateState.createGeometry = true;
}
if (!deepEqual(newProps.unitKinds, currentProps.unitKinds)) {
// console.log('new unitKinds');
updateState.createGeometry = true;

View File

@@ -1 +0,0 @@
// TODO

View File

@@ -11,7 +11,7 @@ import { Theme } from '../../../mol-theme/theme';
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { BitFlags, arrayEqual } from '../../../mol-util';
import { createLinkCylinderImpostors, createLinkCylinderMesh, LinkStyle } from './util/link';
import { createLinkCylinderImpostors, createLinkCylinderMesh, LinkBuilderProps, LinkStyle } from './util/link';
import { ComplexMeshParams, ComplexVisual, ComplexMeshVisual, ComplexCylindersParams, ComplexCylindersVisual } from '../complex-visual';
import { VisualUpdateState } from '../../util';
import { BondType } from '../../../mol-model/structure/model/types';
@@ -34,22 +34,20 @@ function setRefPosition(pos: Vec3, structure: Structure, unit: Unit.Atomic, inde
const tmpRef = Vec3();
function getInterUnitBondCylinderBuilderProps(structure: Structure, theme: Theme, props: PD.Values<InterUnitBondCylinderParams>) {
function getInterUnitBondCylinderBuilderProps(structure: Structure, theme: Theme, props: PD.Values<InterUnitBondCylinderParams>): LinkBuilderProps {
const locE = StructureElement.Location.create(structure);
const locB = Bond.Location(structure, undefined, undefined, structure, undefined, undefined);
const bonds = structure.interUnitBonds;
const { edgeCount, edges } = bonds;
const { sizeFactor, sizeAspectRatio, adjustCylinderLength } = props;
const { sizeFactor, sizeAspectRatio, adjustCylinderLength, aromaticBonds } = props;
const delta = Vec3();
let stub: undefined | ((edgeIndex: number) => boolean);
if (props.includeParent) {
const { child } = structure;
if (!child) throw new Error('expected child to exist');
const { child } = structure;
if (props.includeParent && child) {
stub = (edgeIndex: number) => {
const b = edges[edgeIndex];
const childUnitA = child.unitMap.get(b.unitA);
@@ -137,13 +135,15 @@ function getInterUnitBondCylinderBuilderProps(structure: Structure, theme: Theme
if (BondType.is(f, BondType.Flag.MetallicCoordination) || BondType.is(f, BondType.Flag.HydrogenBond)) {
// show metall coordinations and hydrogen bonds with dashed cylinders
return LinkStyle.Dashed;
} else if (o === 2) {
return LinkStyle.Double;
} else if (o === 3) {
return LinkStyle.Triple;
} else {
return LinkStyle.Solid;
} else if (aromaticBonds && BondType.is(f, BondType.Flag.Aromatic)) {
return LinkStyle.Aromatic;
} else if (o === 2) {
return LinkStyle.Double;
}
return LinkStyle.Solid;
},
radius: (edgeIndex: number) => {
return radius(edgeIndex) * sizeAspectRatio;
@@ -205,6 +205,7 @@ export function InterUnitBondCylinderImpostorVisual(materialId: number): Complex
eachLocation: eachInterBond,
setUpdateState: (state: VisualUpdateState, newProps: PD.Values<InterUnitBondCylinderParams>, currentProps: PD.Values<InterUnitBondCylinderParams>) => {
state.createGeometry = (
newProps.sizeAspectRatio !== currentProps.sizeAspectRatio ||
newProps.linkScale !== currentProps.linkScale ||
newProps.linkSpacing !== currentProps.linkSpacing ||
newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||

View File

@@ -10,7 +10,7 @@ import { Structure, StructureElement, Bond, Unit } from '../../../mol-model/stru
import { Theme } from '../../../mol-theme/theme';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { BitFlags, arrayEqual } from '../../../mol-util';
import { LinkStyle, createLinkLines } from './util/link';
import { LinkStyle, createLinkLines, LinkBuilderProps } from './util/link';
import { ComplexVisual, ComplexLinesVisual, ComplexLinesParams } from '../complex-visual';
import { VisualUpdateState } from '../../util';
import { BondType } from '../../../mol-model/structure/model/types';
@@ -32,14 +32,14 @@ function setRefPosition(pos: Vec3, structure: Structure, unit: Unit.Atomic, inde
function createInterUnitBondLines(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InterUnitBondLineParams>, lines?: Lines) {
const bonds = structure.interUnitBonds;
const { edgeCount, edges } = bonds;
const { sizeFactor } = props;
const { sizeFactor, aromaticBonds } = props;
if (!edgeCount) return Lines.createEmpty(lines);
const ref = Vec3();
const loc = StructureElement.Location.create();
const builderProps = {
const builderProps: LinkBuilderProps = {
linkCount: edgeCount,
referencePosition: (edgeIndex: number) => {
const b = edges[edgeIndex];
@@ -73,13 +73,15 @@ function createInterUnitBondLines(ctx: VisualContext, structure: Structure, them
if (BondType.is(f, BondType.Flag.MetallicCoordination) || BondType.is(f, BondType.Flag.HydrogenBond)) {
// show metall coordinations and hydrogen bonds with dashed cylinders
return LinkStyle.Dashed;
} else if (o === 2) {
return LinkStyle.Double;
} else if (o === 3) {
return LinkStyle.Triple;
} else {
return LinkStyle.Solid;
}else if (aromaticBonds && BondType.is(f, BondType.Flag.Aromatic)) {
return LinkStyle.Aromatic;
} else if (o === 2) {
return LinkStyle.Double;
}
return LinkStyle.Solid;
},
radius: (edgeIndex: number) => {
const b = edges[edgeIndex];

View File

@@ -12,7 +12,7 @@ import { Theme } from '../../../mol-theme/theme';
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { arrayEqual } from '../../../mol-util';
import { createLinkCylinderImpostors, createLinkCylinderMesh, LinkStyle } from './util/link';
import { createLinkCylinderImpostors, createLinkCylinderMesh, LinkBuilderProps, LinkStyle } from './util/link';
import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, StructureGroup, UnitsCylindersParams, UnitsCylindersVisual } from '../units-visual';
import { VisualUpdateState } from '../../util';
import { BondType } from '../../../mol-model/structure/model/types';
@@ -22,16 +22,17 @@ import { IntAdjacencyGraph } from '../../../mol-math/graph';
import { WebGLContext } from '../../../mol-gl/webgl/context';
import { Cylinders } from '../../../mol-geo/geometry/cylinders/cylinders';
import { SortedArray } from '../../../mol-data/int';
import { arrayIntersectionSize } from '../../../mol-util/array';
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
const isBondType = BondType.is;
function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Structure, theme: Theme, props: PD.Values<IntraUnitBondCylinderParams>) {
function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Structure, theme: Theme, props: PD.Values<IntraUnitBondCylinderParams>): LinkBuilderProps {
const elements = unit.elements;
const bonds = unit.bonds;
const { edgeCount, a, b, edgeProps, offset } = bonds;
const { order: _order, flags: _flags } = edgeProps;
const { sizeFactor, sizeAspectRatio, adjustCylinderLength } = props;
const { sizeFactor, sizeAspectRatio, adjustCylinderLength, aromaticBonds } = props;
const vRef = Vec3(), delta = Vec3();
const pos = unit.conformation.invariantPosition;
@@ -41,9 +42,8 @@ function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Stru
const locE = StructureElement.Location.create(structure, unit);
const locB = Bond.Location(structure, unit, undefined, structure, unit, undefined);
if (props.includeParent) {
const { child } = structure;
if (!child) throw new Error('expected child to exist');
const { child } = structure;
if (props.includeParent && child) {
const childUnit = child.unitMap.get(unit.id);
if (!childUnit) throw new Error('expected childUnit to exist');
@@ -70,6 +70,8 @@ function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Stru
return theme.size.size(locE) * sizeFactor;
};
const { elementRingIndices, elementAromaticRingIndices } = unit.rings;
return {
linkCount: edgeCount * 2,
referencePosition: (edgeIndex: number) => {
@@ -77,17 +79,28 @@ function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Stru
if (aI > bI) [aI, bI] = [bI, aI];
if (offset[aI + 1] - offset[aI] === 1) [aI, bI] = [bI, aI];
// TODO prefer reference atoms within rings
const aR = elementRingIndices.get(aI);
let maxSize = 0;
for (let i = offset[aI], il = offset[aI + 1]; i < il; ++i) {
const _bI = b[i];
if (_bI !== bI && _bI !== aI) return pos(elements[_bI], vRef);
if (_bI !== bI && _bI !== aI) {
if (aR) {
const _bR = elementRingIndices.get(_bI);
if (!_bR) continue;
const size = arrayIntersectionSize(aR, _bR);
if (size > maxSize) {
maxSize = size;
pos(elements[_bI], vRef);
}
} else {
return pos(elements[_bI], vRef);
}
}
}
for (let i = offset[bI], il = offset[bI + 1]; i < il; ++i) {
const _aI = a[i];
if (_aI !== aI && _aI !== bI) return pos(elements[_aI], vRef);
}
return null;
return maxSize > 0 ? vRef : null;
},
position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
pos(elements[a[edgeIndex]], posA);
@@ -111,13 +124,24 @@ function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Stru
if (isBondType(f, BondType.Flag.MetallicCoordination) || isBondType(f, BondType.Flag.HydrogenBond)) {
// show metall coordinations and hydrogen bonds with dashed cylinders
return LinkStyle.Dashed;
} else if (o === 2) {
return LinkStyle.Double;
} else if (o === 3) {
return LinkStyle.Triple;
} else {
return LinkStyle.Solid;
} else if (aromaticBonds) {
const aI = a[edgeIndex], bI = b[edgeIndex];
const aR = elementAromaticRingIndices.get(aI);
const bR = elementAromaticRingIndices.get(bI);
const arCount = (aR && bR) ? arrayIntersectionSize(aR, bR) : 0;
if (arCount || isBondType(f, BondType.Flag.Aromatic)) {
if (arCount === 2) {
return LinkStyle.MirroredAromatic;
} else {
return LinkStyle.Aromatic;
}
}
}
return o === 2 ? LinkStyle.Double : LinkStyle.Solid;
},
radius: (edgeIndex: number) => {
return radius(edgeIndex) * sizeAspectRatio;
@@ -187,6 +211,7 @@ export function IntraUnitBondCylinderImpostorVisual(materialId: number): UnitsVi
eachLocation: eachIntraBond,
setUpdateState: (state: VisualUpdateState, newProps: PD.Values<IntraUnitBondCylinderParams>, currentProps: PD.Values<IntraUnitBondCylinderParams>, newTheme: Theme, currentTheme: Theme, newStructureGroup: StructureGroup, currentStructureGroup: StructureGroup) => {
state.createGeometry = (
newProps.sizeAspectRatio !== currentProps.sizeAspectRatio ||
newProps.linkScale !== currentProps.linkScale ||
newProps.linkSpacing !== currentProps.linkSpacing ||
newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
@@ -197,7 +222,8 @@ export function IntraUnitBondCylinderImpostorVisual(materialId: number): UnitsVi
newProps.stubCap !== currentProps.stubCap ||
!arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
!arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) ||
newProps.adjustCylinderLength !== currentProps.adjustCylinderLength
newProps.adjustCylinderLength !== currentProps.adjustCylinderLength ||
newProps.aromaticBonds !== currentProps.aromaticBonds
);
const newUnit = newStructureGroup.group.units[0];
@@ -239,7 +265,8 @@ export function IntraUnitBondCylinderMeshVisual(materialId: number): UnitsVisual
newProps.stubCap !== currentProps.stubCap ||
!arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
!arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) ||
newProps.adjustCylinderLength !== currentProps.adjustCylinderLength
newProps.adjustCylinderLength !== currentProps.adjustCylinderLength ||
newProps.aromaticBonds !== currentProps.aromaticBonds
);
const newUnit = newStructureGroup.group.units[0];

View File

@@ -10,7 +10,7 @@ import { Unit, Structure, StructureElement } from '../../../mol-model/structure'
import { Theme } from '../../../mol-theme/theme';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { arrayEqual } from '../../../mol-util';
import { LinkStyle, createLinkLines } from './util/link';
import { LinkStyle, createLinkLines, LinkBuilderProps } from './util/link';
import { UnitsVisual, UnitsLinesParams, UnitsLinesVisual, StructureGroup } from '../units-visual';
import { VisualUpdateState } from '../../util';
import { BondType } from '../../../mol-model/structure/model/types';
@@ -18,6 +18,7 @@ import { BondIterator, BondLineParams, getIntraBondLoci, eachIntraBond, makeIntr
import { Sphere3D } from '../../../mol-math/geometry';
import { Lines } from '../../../mol-geo/geometry/lines/lines';
import { IntAdjacencyGraph } from '../../../mol-math/graph';
import { arrayIntersectionSize } from '../../../mol-util/array';
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
const isBondType = BondType.is;
@@ -29,41 +30,50 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str
const childUnit = child?.unitMap.get(unit.id);
if (child && !childUnit) return Lines.createEmpty(lines);
if (props.includeParent) {
if (!child) throw new Error('expected child to exist');
}
const location = StructureElement.Location.create(structure, unit);
const elements = unit.elements;
const bonds = unit.bonds;
const { edgeCount, a, b, edgeProps, offset } = bonds;
const { order: _order, flags: _flags } = edgeProps;
const { sizeFactor } = props;
const { sizeFactor, aromaticBonds } = props;
if (!edgeCount) return Lines.createEmpty(lines);
const vRef = Vec3();
const pos = unit.conformation.invariantPosition;
const builderProps = {
const { elementRingIndices, elementAromaticRingIndices } = unit.rings;
const builderProps: LinkBuilderProps = {
linkCount: edgeCount * 2,
referencePosition: (edgeIndex: number) => {
let aI = a[edgeIndex], bI = b[edgeIndex];
if (aI > bI) [aI, bI] = [bI, aI];
if (offset[aI + 1] - offset[aI] === 1) [aI, bI] = [bI, aI];
// TODO prefer reference atoms within rings
const aR = elementRingIndices.get(aI);
let maxSize = 0;
for (let i = offset[aI], il = offset[aI + 1]; i < il; ++i) {
const _bI = b[i];
if (_bI !== bI && _bI !== aI) return pos(elements[_bI], vRef);
if (_bI !== bI && _bI !== aI) {
if (aR) {
const _bR = elementRingIndices.get(_bI);
if (!_bR) continue;
const size = arrayIntersectionSize(aR, _bR);
if (size > maxSize) {
maxSize = size;
pos(elements[_bI], vRef);
}
} else {
return pos(elements[_bI], vRef);
}
}
}
for (let i = offset[bI], il = offset[bI + 1]; i < il; ++i) {
const _aI = a[i];
if (_aI !== aI && _aI !== bI) return pos(elements[_aI], vRef);
}
return null;
return maxSize > 0 ? vRef : null;
},
position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
pos(elements[a[edgeIndex]], posA);
@@ -75,13 +85,24 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str
if (isBondType(f, BondType.Flag.MetallicCoordination) || isBondType(f, BondType.Flag.HydrogenBond)) {
// show metall coordinations and hydrogen bonds with dashed cylinders
return LinkStyle.Dashed;
} else if (o === 2) {
return LinkStyle.Double;
} else if (o === 3) {
return LinkStyle.Triple;
} else {
return LinkStyle.Solid;
} else if (aromaticBonds) {
const aI = a[edgeIndex], bI = b[edgeIndex];
const aR = elementAromaticRingIndices.get(aI);
const bR = elementAromaticRingIndices.get(bI);
const arCount = (aR && bR) ? arrayIntersectionSize(aR, bR) : 0;
if (arCount || isBondType(f, BondType.Flag.Aromatic)) {
if (arCount === 2) {
return LinkStyle.MirroredAromatic;
} else {
return LinkStyle.Aromatic;
}
}
}
return o === 2 ? LinkStyle.Double : LinkStyle.Solid;
},
radius: (edgeIndex: number) => {
location.element = elements[a[edgeIndex]];
@@ -123,7 +144,8 @@ export function IntraUnitBondLineVisual(materialId: number): UnitsVisual<IntraUn
newProps.dashCount !== currentProps.dashCount ||
newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
!arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
!arrayEqual(newProps.excludeTypes, currentProps.excludeTypes)
!arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) ||
newProps.aromaticBonds !== currentProps.aromaticBonds
);
const newUnit = newStructureGroup.group.units[0];

View File

@@ -1,34 +1,102 @@
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { VisualContext } from '../../visual';
import { Unit, Structure } from '../../../mol-model/structure';
import { Unit, Structure, StructureElement, ElementIndex } from '../../../mol-model/structure';
import { Theme } from '../../../mol-theme/theme';
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { CylinderProps } from '../../../mol-geo/primitive/cylinder';
import { PolymerBackboneIterator } from './util/polymer';
import { OrderedSet } from '../../../mol-data/int';
import { eachPolymerElement, getPolymerElementLoci, NucleicShift, PolymerLocationIterator, StandardShift } from './util/polymer';
import { addCylinder } from '../../../mol-geo/geometry/mesh/builder/cylinder';
import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual } from '../units-visual';
import { ElementIterator, getElementLoci, eachElement } from './util/element';
import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, UnitsCylindersVisual, UnitsCylindersParams, StructureGroup } from '../units-visual';
import { VisualUpdateState } from '../../util';
import { BaseGeometry } from '../../../mol-geo/geometry/base';
import { Sphere3D } from '../../../mol-math/geometry';
import { isNucleic, MoleculeType } from '../../../mol-model/structure/model/types';
import { WebGLContext } from '../../../mol-gl/webgl/context';
import { Cylinders } from '../../../mol-geo/geometry/cylinders/cylinders';
import { CylindersBuilder } from '../../../mol-geo/geometry/cylinders/cylinders-builder';
import { eachPolymerBackboneLink } from './util/polymer/backbone';
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
const v3scale = Vec3.scale;
const v3add = Vec3.add;
const v3sub = Vec3.sub;
export const PolymerBackboneCylinderParams = {
...UnitsMeshParams,
...UnitsCylindersParams,
sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
radialSegments: PD.Numeric(16, { min: 2, max: 56, step: 2 }, BaseGeometry.CustomQualityParamInfo),
tryUseImpostor: PD.Boolean(true),
};
export const DefaultPolymerBackboneCylinderProps = PD.getDefaultValues(PolymerBackboneCylinderParams);
export type PolymerBackboneCylinderProps = typeof DefaultPolymerBackboneCylinderProps
export type PolymerBackboneCylinderParams = typeof PolymerBackboneCylinderParams
export function PolymerBackboneCylinderVisual(materialId: number, structure: Structure, props: PD.Values<PolymerBackboneCylinderParams>, webgl?: WebGLContext) {
return props.tryUseImpostor && webgl && webgl.extensions.fragDepth
? PolymerBackboneCylinderImpostorVisual(materialId)
: PolymerBackboneCylinderMeshVisual(materialId);
}
interface PolymerBackboneCylinderProps {
radialSegments: number,
sizeFactor: number,
}
function createPolymerBackboneCylinderImpostor(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PolymerBackboneCylinderProps, cylinders?: Cylinders) {
const polymerElementCount = unit.polymerElements.length;
if (!polymerElementCount) return Cylinders.createEmpty(cylinders);
const cylindersCountEstimate = polymerElementCount * 2;
const builder = CylindersBuilder.create(cylindersCountEstimate, cylindersCountEstimate / 4, cylinders);
const pos = unit.conformation.invariantPosition;
const pA = Vec3();
const pB = Vec3();
const pM = Vec3();
const add = function(indexA: ElementIndex, indexB: ElementIndex, groupA: number, groupB: number, moleculeType: MoleculeType) {
pos(indexA, pA);
pos(indexB, pB);
const isNucleicType = isNucleic(moleculeType);
const shift = isNucleicType ? NucleicShift : StandardShift;
v3add(pM, pA, v3scale(pM, v3sub(pM, pB, pA), shift));
builder.add(pA[0], pA[1], pA[2], pM[0], pM[1], pM[2], 1, false, false, groupA);
builder.add(pM[0], pM[1], pM[2], pB[0], pB[1], pB[2], 1, false, false, groupB);
};
eachPolymerBackboneLink(unit, add);
const c = builder.getCylinders();
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * props.sizeFactor);
c.setBoundingSphere(sphere);
return c;
}
export function PolymerBackboneCylinderImpostorVisual(materialId: number): UnitsVisual<PolymerBackboneCylinderParams> {
return UnitsCylindersVisual<PolymerBackboneCylinderParams>({
defaultProps: PD.getDefaultValues(PolymerBackboneCylinderParams),
createGeometry: createPolymerBackboneCylinderImpostor,
createLocationIterator: PolymerLocationIterator.fromGroup,
getLoci: getPolymerElementLoci,
eachLocation: eachPolymerElement,
setUpdateState: (state: VisualUpdateState, newProps: PD.Values<PolymerBackboneCylinderParams>, currentProps: PD.Values<PolymerBackboneCylinderParams>) => { },
mustRecreate: (structureGroup: StructureGroup, props: PD.Values<PolymerBackboneCylinderParams>, webgl?: WebGLContext) => {
return !props.tryUseImpostor || !webgl;
}
}, materialId);
}
// TODO do group id based on polymer index not element index
function createPolymerBackboneCylinderMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PolymerBackboneCylinderProps, mesh?: Mesh) {
const polymerElementCount = unit.polymerElements.length;
if (!polymerElementCount) return Mesh.createEmpty(mesh);
@@ -38,26 +106,34 @@ function createPolymerBackboneCylinderMesh(ctx: VisualContext, unit: Unit, struc
const vertexCountEstimate = radialSegments * 2 * polymerElementCount * 2;
const builderState = MeshBuilder.createState(vertexCountEstimate, vertexCountEstimate / 10, mesh);
const { elements } = unit;
const pos = unit.conformation.invariantPosition;
const pA = Vec3.zero();
const pB = Vec3.zero();
const pA = Vec3();
const pB = Vec3();
const cylinderProps: CylinderProps = { radiusTop: 1, radiusBottom: 1, radialSegments };
const polymerBackboneIt = PolymerBackboneIterator(structure, unit);
while (polymerBackboneIt.hasNext) {
const { centerA, centerB } = polymerBackboneIt.move();
const centerA = StructureElement.Location.create(structure, unit);
const centerB = StructureElement.Location.create(structure, unit);
const add = function(indexA: ElementIndex, indexB: ElementIndex, groupA: number, groupB: number, moleculeType: MoleculeType) {
centerA.element = indexA;
centerB.element = indexB;
pos(centerA.element, pA);
pos(centerB.element, pB);
const isNucleicType = isNucleic(moleculeType);
const shift = isNucleicType ? NucleicShift : StandardShift;
cylinderProps.radiusTop = cylinderProps.radiusBottom = theme.size.size(centerA) * sizeFactor;
builderState.currentGroup = OrderedSet.indexOf(elements, centerA.element);
addCylinder(builderState, pA, pB, 0.5, cylinderProps);
builderState.currentGroup = groupA;
addCylinder(builderState, pA, pB, shift, cylinderProps);
cylinderProps.radiusTop = cylinderProps.radiusBottom = theme.size.size(centerB) * sizeFactor;
builderState.currentGroup = OrderedSet.indexOf(elements, centerB.element);
addCylinder(builderState, pB, pA, 0.5, cylinderProps);
}
builderState.currentGroup = groupB;
addCylinder(builderState, pB, pA, 1 - shift, cylinderProps);
};
eachPolymerBackboneLink(unit, add);
const m = MeshBuilder.getMesh(builderState);
@@ -67,25 +143,21 @@ function createPolymerBackboneCylinderMesh(ctx: VisualContext, unit: Unit, struc
return m;
}
export const PolymerBackboneParams = {
...UnitsMeshParams,
...PolymerBackboneCylinderParams,
};
export type PolymerBackboneParams = typeof PolymerBackboneParams
export function PolymerBackboneVisual(materialId: number): UnitsVisual<PolymerBackboneParams> {
return UnitsMeshVisual<PolymerBackboneParams>({
defaultProps: PD.getDefaultValues(PolymerBackboneParams),
export function PolymerBackboneCylinderMeshVisual(materialId: number): UnitsVisual<PolymerBackboneCylinderParams> {
return UnitsMeshVisual<PolymerBackboneCylinderParams>({
defaultProps: PD.getDefaultValues(PolymerBackboneCylinderParams),
createGeometry: createPolymerBackboneCylinderMesh,
// TODO create a specialized location iterator
createLocationIterator: ElementIterator.fromGroup,
getLoci: getElementLoci,
eachLocation: eachElement,
setUpdateState: (state: VisualUpdateState, newProps: PD.Values<PolymerBackboneParams>, currentProps: PD.Values<PolymerBackboneParams>) => {
createLocationIterator: PolymerLocationIterator.fromGroup,
getLoci: getPolymerElementLoci,
eachLocation: eachPolymerElement,
setUpdateState: (state: VisualUpdateState, newProps: PD.Values<PolymerBackboneCylinderParams>, currentProps: PD.Values<PolymerBackboneCylinderParams>) => {
state.createGeometry = (
newProps.sizeFactor !== currentProps.sizeFactor ||
newProps.radialSegments !== currentProps.radialSegments
);
},
mustRecreate: (structureGroup: StructureGroup, props: PD.Values<PolymerBackboneCylinderParams>, webgl?: WebGLContext) => {
return props.tryUseImpostor && !!webgl;
}
}, materialId);
}

View File

@@ -1 +1,131 @@
// TODO
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { VisualContext } from '../../visual';
import { Unit, Structure, ElementIndex, StructureElement } from '../../../mol-model/structure';
import { Theme } from '../../../mol-theme/theme';
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { eachPolymerElement, getPolymerElementLoci, PolymerLocationIterator } from './util/polymer';
import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, UnitsSpheresVisual, UnitsSpheresParams, StructureGroup } from '../units-visual';
import { VisualUpdateState } from '../../util';
import { BaseGeometry } from '../../../mol-geo/geometry/base';
import { Sphere3D } from '../../../mol-math/geometry';
import { addSphere } from '../../../mol-geo/geometry/mesh/builder/sphere';
import { sphereVertexCount } from '../../../mol-geo/primitive/sphere';
import { WebGLContext } from '../../../mol-gl/webgl/context';
import { Spheres } from '../../../mol-geo/geometry/spheres/spheres';
import { SpheresBuilder } from '../../../mol-geo/geometry/spheres/spheres-builder';
import { eachPolymerBackboneElement } from './util/polymer/backbone';
export const PolymerBackboneSphereParams = {
...UnitsMeshParams,
...UnitsSpheresParams,
sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }, BaseGeometry.CustomQualityParamInfo),
tryUseImpostor: PD.Boolean(true),
};
export type PolymerBackboneSphereParams = typeof PolymerBackboneSphereParams
export function PolymerBackboneSphereVisual(materialId: number, structure: Structure, props: PD.Values<PolymerBackboneSphereParams>, webgl?: WebGLContext) {
return props.tryUseImpostor && webgl && webgl.extensions.fragDepth
? PolymerBackboneSphereImpostorVisual(materialId)
: PolymerBackboneSphereMeshVisual(materialId);
}
interface PolymerBackboneSphereProps {
detail: number,
sizeFactor: number,
}
function createPolymerBackboneSphereImpostor(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PolymerBackboneSphereProps, spheres?: Spheres) {
const polymerElementCount = unit.polymerElements.length;
if (!polymerElementCount) return Spheres.createEmpty(spheres);
const builder = SpheresBuilder.create(polymerElementCount, polymerElementCount / 2, spheres);
const pos = unit.conformation.invariantPosition;
const p = Vec3();
const add = (index: ElementIndex, group: number) => {
pos(index, p);
builder.add(p[0], p[1], p[2], group);
};
eachPolymerBackboneElement(unit, add);
const s = builder.getSpheres();
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * props.sizeFactor);
s.setBoundingSphere(sphere);
return s;
}
export function PolymerBackboneSphereImpostorVisual(materialId: number): UnitsVisual<PolymerBackboneSphereParams> {
return UnitsSpheresVisual<PolymerBackboneSphereParams>({
defaultProps: PD.getDefaultValues(PolymerBackboneSphereParams),
createGeometry: createPolymerBackboneSphereImpostor,
createLocationIterator: PolymerLocationIterator.fromGroup,
getLoci: getPolymerElementLoci,
eachLocation: eachPolymerElement,
setUpdateState: (state: VisualUpdateState, newProps: PD.Values<PolymerBackboneSphereParams>, currentProps: PD.Values<PolymerBackboneSphereParams>) => { },
mustRecreate: (structureGroup: StructureGroup, props: PD.Values<PolymerBackboneSphereParams>, webgl?: WebGLContext) => {
return !props.tryUseImpostor || !webgl;
}
}, materialId);
}
function createPolymerBackboneSphereMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PolymerBackboneSphereProps, mesh?: Mesh) {
const polymerElementCount = unit.polymerElements.length;
if (!polymerElementCount) return Mesh.createEmpty(mesh);
const { detail, sizeFactor } = props;
const vertexCount = polymerElementCount * sphereVertexCount(detail);
const builderState = MeshBuilder.createState(vertexCount, vertexCount / 2, mesh);
const pos = unit.conformation.invariantPosition;
const p = Vec3();
const center = StructureElement.Location.create(structure, unit);
const add = (index: ElementIndex, group: number) => {
center.element = index;
pos(center.element, p);
builderState.currentGroup = group;
addSphere(builderState, p, theme.size.size(center) * sizeFactor, detail);
};
eachPolymerBackboneElement(unit, add);
const m = MeshBuilder.getMesh(builderState);
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * props.sizeFactor);
m.setBoundingSphere(sphere);
return m;
}
export function PolymerBackboneSphereMeshVisual(materialId: number): UnitsVisual<PolymerBackboneSphereParams> {
return UnitsMeshVisual<PolymerBackboneSphereParams>({
defaultProps: PD.getDefaultValues(PolymerBackboneSphereParams),
createGeometry: createPolymerBackboneSphereMesh,
createLocationIterator: PolymerLocationIterator.fromGroup,
getLoci: getPolymerElementLoci,
eachLocation: eachPolymerElement,
setUpdateState: (state: VisualUpdateState, newProps: PD.Values<PolymerBackboneSphereParams>, currentProps: PD.Values<PolymerBackboneSphereParams>) => {
state.createGeometry = (
newProps.sizeFactor !== currentProps.sizeFactor ||
newProps.detail !== currentProps.detail
);
},
mustRecreate: (structureGroup: StructureGroup, props: PD.Values<PolymerBackboneSphereParams>, webgl?: WebGLContext) => {
return props.tryUseImpostor && !!webgl;
}
}, materialId);
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -27,6 +27,7 @@ export const PolymerTraceMeshParams = {
sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
aspectRatio: PD.Numeric(5, { min: 0.1, max: 10, step: 0.1 }),
arrowFactor: PD.Numeric(1.5, { min: 0, max: 3, step: 0.1 }),
tubularHelices: PD.Boolean(false),
detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }, BaseGeometry.CustomQualityParamInfo),
linearSegments: PD.Numeric(8, { min: 1, max: 48, step: 1 }, BaseGeometry.CustomQualityParamInfo),
radialSegments: PD.Numeric(16, { min: 2, max: 56, step: 2 }, BaseGeometry.CustomQualityParamInfo)
@@ -40,7 +41,7 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
const polymerElementCount = unit.polymerElements.length;
if (!polymerElementCount) return Mesh.createEmpty(mesh);
const { sizeFactor, detail, linearSegments, radialSegments, aspectRatio, arrowFactor } = props;
const { sizeFactor, detail, linearSegments, radialSegments, aspectRatio, arrowFactor, tubularHelices } = props;
const vertexCount = linearSegments * radialSegments * polymerElementCount + (radialSegments + 1) * polymerElementCount * 2;
const builderState = MeshBuilder.createState(vertexCount, vertexCount / 10, mesh);
@@ -50,7 +51,7 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
const { curvePoints, normalVectors, binormalVectors, widthValues, heightValues } = state;
let i = 0;
const polymerTraceIt = PolymerTraceIterator(unit, structure);
const polymerTraceIt = PolymerTraceIterator(unit, structure, { ignoreSecondaryStructure: false, useHelixOrientation: tubularHelices });
while (polymerTraceIt.hasNext) {
const v = polymerTraceIt.move();
builderState.currentGroup = i;
@@ -58,7 +59,7 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
const isNucleicType = isNucleic(v.moleculeType);
const isSheet = SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Beta);
const isHelix = SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Helix);
const tension = isHelix ? HelixTension : StandardTension;
const tension = isHelix && !tubularHelices ? HelixTension : StandardTension;
const shift = isNucleicType ? NucleicShift : StandardShift;
interpolateCurveSegment(state, v, tension, shift);
@@ -112,9 +113,19 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
} else {
let h0: number, h1: number, h2: number;
if (isHelix && !v.isCoarseBackbone) {
h0 = w0 * aspectRatio;
h1 = w1 * aspectRatio;
h2 = w2 * aspectRatio;
if (tubularHelices) {
w0 *= aspectRatio * 1.5;
w1 *= aspectRatio * 1.5;
w2 *= aspectRatio * 1.5;
h0 = w0;
h1 = w1;
h2 = w2;
} else {
h0 = w0 * aspectRatio;
h1 = w1 * aspectRatio;
h2 = w2 * aspectRatio;
}
} else if (isNucleicType && !v.isCoarseBackbone) {
h0 = w0 * aspectRatio;
h1 = w1 * aspectRatio;
@@ -172,6 +183,7 @@ export function PolymerTraceVisual(materialId: number): UnitsVisual<PolymerTrace
setUpdateState: (state: VisualUpdateState, newProps: PD.Values<PolymerTraceParams>, currentProps: PD.Values<PolymerTraceParams>, newTheme: Theme, currentTheme: Theme, newStructureGroup: StructureGroup, currentStructureGroup: StructureGroup) => {
state.createGeometry = (
newProps.sizeFactor !== currentProps.sizeFactor ||
newProps.tubularHelices !== currentProps.tubularHelices ||
newProps.detail !== currentProps.detail ||
newProps.linearSegments !== currentProps.linearSegments ||
newProps.radialSegments !== currentProps.radialSegments ||

View File

@@ -46,7 +46,7 @@ function createPolymerTubeMesh(ctx: VisualContext, unit: Unit, structure: Struct
const { curvePoints, normalVectors, binormalVectors, widthValues, heightValues } = state;
let i = 0;
const polymerTraceIt = PolymerTraceIterator(unit, structure, true);
const polymerTraceIt = PolymerTraceIterator(unit, structure, { ignoreSecondaryStructure: true });
while (polymerTraceIt.hasNext) {
const v = polymerTraceIt.move();
builderState.currentGroup = i;

View File

@@ -20,6 +20,7 @@ export const BondParams = {
includeTypes: PD.MultiSelect(ObjectKeys(BondType.Names), PD.objectToOptions(BondType.Names)),
excludeTypes: PD.MultiSelect([] as BondType.Names[], PD.objectToOptions(BondType.Names)),
ignoreHydrogens: PD.Boolean(false),
aromaticBonds: PD.Boolean(false, { description: 'Display aromatic bonds with dashes' }),
};
export const DefaultBondProps = PD.getDefaultValues(BondParams);
export type BondProps = typeof DefaultBondProps

View File

@@ -46,7 +46,7 @@ export function makeElementIgnoreTest(structure: Structure, unit: Unit, props: E
const childUnit = child?.unitMap.get(unit.id);
if (child && !childUnit) throw new Error('expected childUnit to exist if child exists');
if (!child && ((!ignoreHydrogens && !traceOnly) || traceOnly)) return;
if (!child && !ignoreHydrogens && !traceOnly) return;
return (element: ElementIndex) => {
return (

View File

@@ -18,7 +18,7 @@ import { Cylinders } from '../../../../mol-geo/geometry/cylinders/cylinders';
import { CylindersBuilder } from '../../../../mol-geo/geometry/cylinders/cylinders-builder';
export const LinkCylinderParams = {
linkScale: PD.Numeric(0.4, { min: 0, max: 1, step: 0.1 }),
linkScale: PD.Numeric(0.45, { min: 0, max: 1, step: 0.01 }),
linkSpacing: PD.Numeric(1, { min: 0, max: 2, step: 0.01 }),
linkCap: PD.Boolean(false),
dashCount: PD.Numeric(4, { min: 2, max: 10, step: 2 }),
@@ -84,7 +84,9 @@ export const enum LinkStyle {
Dashed = 1,
Double = 2,
Triple = 3,
Disk = 4
Disk = 4,
Aromatic = 5,
MirroredAromatic = 6,
}
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
@@ -133,6 +135,8 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
const [topCap, bottomCap] = (v3dot(tmpV12, up) > 0) ? [linkStub, linkCap] : [linkCap, linkStub];
builderState.currentGroup = edgeIndex;
const aromaticOffsetFactor = 4.5;
if (linkStyle === LinkStyle.Solid) {
cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius;
cylinderProps.topCap = topCap;
@@ -144,20 +148,41 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
cylinderProps.topCap = cylinderProps.bottomCap = dashCap;
addFixedCountDashedCylinder(builderState, va, vb, 0.5, segmentCount, cylinderProps);
} else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple) {
const order = linkStyle === LinkStyle.Double ? 2 : 3;
} else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
const order = linkStyle === LinkStyle.Double ? 2 :
linkStyle === LinkStyle.Triple ? 3 : 1.5;
const multiRadius = linkRadius * (linkScale / (0.5 * order));
const absOffset = (linkRadius - multiRadius) * linkSpacing;
calculateShiftDir(vShift, va, vb, referencePosition ? referencePosition(edgeIndex) : null);
v3setMagnitude(vShift, vShift, absOffset);
cylinderProps.radiusTop = cylinderProps.radiusBottom = multiRadius;
cylinderProps.topCap = topCap;
cylinderProps.bottomCap = bottomCap;
if (order === 3) addCylinder(builderState, va, vb, 0.5, cylinderProps);
addDoubleCylinder(builderState, va, vb, 0.5, vShift, cylinderProps);
if (linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius;
addCylinder(builderState, va, vb, 0.5, cylinderProps);
cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius * linkScale;
cylinderProps.topCap = cylinderProps.bottomCap = dashCap;
v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor);
v3sub(va, va, vShift);
v3sub(vb, vb, vShift);
addFixedCountDashedCylinder(builderState, va, vb, 0.5, 3, cylinderProps);
if (linkStyle === LinkStyle.MirroredAromatic) {
v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor * 2);
v3add(va, va, vShift);
v3add(vb, vb, vShift);
addFixedCountDashedCylinder(builderState, va, vb, 0.5, 3, cylinderProps);
}
} else {
v3setMagnitude(vShift, vShift, absOffset);
cylinderProps.radiusTop = cylinderProps.radiusBottom = multiRadius;
if (order === 3) addCylinder(builderState, va, vb, 0.5, cylinderProps);
addDoubleCylinder(builderState, va, vb, 0.5, vShift, cylinderProps);
}
} else if (linkStyle === LinkStyle.Disk) {
v3scale(tmpV12, tmpV12, 0.475);
v3add(va, va, tmpV12);
@@ -190,12 +215,17 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin
const va = Vec3();
const vb = Vec3();
const vm = Vec3();
const vShift = Vec3();
// automatically adjust length for evenly spaced dashed cylinders
const segmentCount = dashCount % 2 === 1 ? dashCount : dashCount + 1;
const lengthScale = 0.5 - (0.5 / 2 / segmentCount);
const aromaticSegmentCount = 3;
const aromaticLengthScale = 0.5 - (0.5 / 2 / aromaticSegmentCount);
const aromaticOffsetFactor = 4.5;
for (let edgeIndex = 0, _eI = linkCount; edgeIndex < _eI; ++edgeIndex) {
if (ignore && ignore(edgeIndex)) continue;
@@ -206,24 +236,45 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin
const linkStub = stubCap && (stub ? stub(edgeIndex) : false);
if (linkStyle === LinkStyle.Solid) {
v3scale(vb, v3add(vb, va, vb), 0.5);
builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], 1, linkCap, linkStub, edgeIndex);
v3scale(vm, v3add(vm, va, vb), 0.5);
builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], 1, linkCap, linkStub, edgeIndex);
} else if (linkStyle === LinkStyle.Dashed) {
v3scale(tmpV12, v3sub(tmpV12, vb, va), lengthScale);
v3sub(vb, vb, tmpV12);
builder.addFixedCountDashes(va, vb, segmentCount, dashScale, dashCap, dashCap, edgeIndex);
} else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple) {
v3scale(vb, v3add(vb, va, vb), 0.5);
const order = linkStyle === LinkStyle.Double ? 2 : 3;
} else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
const order = linkStyle === LinkStyle.Double ? 2 :
linkStyle === LinkStyle.Triple ? 3 : 1.5;
const multiScale = linkScale / (0.5 * order);
const absOffset = (linkRadius - multiScale * linkRadius) * linkSpacing;
v3scale(vm, v3add(vm, va, vb), 0.5);
calculateShiftDir(vShift, va, vb, referencePosition ? referencePosition(edgeIndex) : null);
v3setMagnitude(vShift, vShift, absOffset);
if (order === 3) builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], multiScale, linkCap, false, edgeIndex);
builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vb[0] + vShift[0], vb[1] + vShift[1], vb[2] + vShift[2], multiScale, linkCap, linkStub, edgeIndex);
builder.add(va[0] - vShift[0], va[1] - vShift[1], va[2] - vShift[2], vb[0] - vShift[0], vb[1] - vShift[1], vb[2] - vShift[2], multiScale, linkCap, linkStub, edgeIndex);
if (linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], 1, linkCap, linkStub, edgeIndex);
v3scale(tmpV12, v3sub(tmpV12, vb, va), aromaticLengthScale);
v3sub(vb, vb, tmpV12);
v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor);
v3sub(va, va, vShift);
v3sub(vb, vb, vShift);
builder.addFixedCountDashes(va, vb, aromaticSegmentCount, linkScale, dashCap, dashCap, edgeIndex);
if (linkStyle === LinkStyle.MirroredAromatic) {
v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor * 2);
v3add(va, va, vShift);
v3add(vb, vb, vShift);
builder.addFixedCountDashes(va, vb, aromaticSegmentCount, linkScale, dashCap, dashCap, edgeIndex);
}
} else {
v3setMagnitude(vShift, vShift, absOffset);
if (order === 3) builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], multiScale, linkCap, false, edgeIndex);
builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vm[0] + vShift[0], vm[1] + vShift[1], vm[2] + vShift[2], multiScale, linkCap, linkStub, edgeIndex);
builder.add(va[0] - vShift[0], va[1] - vShift[1], va[2] - vShift[2], vm[0] - vShift[0], vm[1] - vShift[1], vm[2] - vShift[2], multiScale, linkCap, linkStub, edgeIndex);
}
} else if (linkStyle === LinkStyle.Disk) {
v3scale(tmpV12, v3sub(tmpV12, vb, va), 0.475);
v3add(va, va, tmpV12);
@@ -252,12 +303,17 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp
const va = Vec3();
const vb = Vec3();
const vm = Vec3();
const vShift = Vec3();
// automatically adjust length for evenly spaced dashed lines
const segmentCount = dashCount % 2 === 1 ? dashCount : dashCount + 1;
const lengthScale = 0.5 - (0.5 / 2 / segmentCount);
const aromaticSegmentCount = 3;
const aromaticLengthScale = 0.5 - (0.5 / 2 / aromaticSegmentCount);
const aromaticOffsetFactor = 4.5;
for (let edgeIndex = 0, _eI = linkCount; edgeIndex < _eI; ++edgeIndex) {
if (ignore && ignore(edgeIndex)) continue;
@@ -266,24 +322,45 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp
const linkStyle = style ? style(edgeIndex) : LinkStyle.Solid;
if (linkStyle === LinkStyle.Solid) {
v3scale(vb, v3add(vb, va, vb), 0.5);
builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], edgeIndex);
v3scale(vm, v3add(vm, va, vb), 0.5);
builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], edgeIndex);
} else if (linkStyle === LinkStyle.Dashed) {
v3scale(tmpV12, v3sub(tmpV12, vb, va), lengthScale);
v3sub(vb, vb, tmpV12);
builder.addFixedCountDashes(va, vb, segmentCount, edgeIndex);
} else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple) {
v3scale(vb, v3add(vb, va, vb), 0.5);
const order = linkStyle === LinkStyle.Double ? 2 : 3;
} else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
const order = linkStyle === LinkStyle.Double ? 2 :
linkStyle === LinkStyle.Triple ? 3 : 1.5;
const multiRadius = 1 * (linkScale / (0.5 * order));
const absOffset = (1 - multiRadius) * linkSpacing;
v3scale(vm, v3add(vm, va, vb), 0.5);
calculateShiftDir(vShift, va, vb, referencePosition ? referencePosition(edgeIndex) : null);
v3setMagnitude(vShift, vShift, absOffset);
if (order === 3) builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], edgeIndex);
builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vb[0] + vShift[0], vb[1] + vShift[1], vb[2] + vShift[2], edgeIndex);
builder.add(va[0] - vShift[0], va[1] - vShift[1], va[2] - vShift[2], vb[0] - vShift[0], vb[1] - vShift[1], vb[2] - vShift[2], edgeIndex);
if (linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], edgeIndex);
v3scale(tmpV12, v3sub(tmpV12, vb, va), aromaticLengthScale);
v3sub(vb, vb, tmpV12);
v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor);
v3sub(va, va, vShift);
v3sub(vb, vb, vShift);
builder.addFixedCountDashes(va, vb, aromaticSegmentCount, edgeIndex);
if (linkStyle === LinkStyle.MirroredAromatic) {
v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor * 2);
v3add(va, va, vShift);
v3add(vb, vb, vShift);
builder.addFixedCountDashes(va, vb, aromaticSegmentCount, edgeIndex);
}
} else {
v3setMagnitude(vShift, vShift, absOffset);
if (order === 3) builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], edgeIndex);
builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vm[0] + vShift[0], vm[1] + vShift[1], vm[2] + vShift[2], edgeIndex);
builder.add(va[0] - vShift[0], va[1] - vShift[1], va[2] - vShift[2], vm[0] - vShift[0], vm[1] - vShift[1], vm[2] - vShift[2], edgeIndex);
}
} else if (linkStyle === LinkStyle.Disk) {
v3scale(tmpV12, v3sub(tmpV12, vb, va), 0.475);
v3add(va, va, tmpV12);

View File

@@ -14,7 +14,7 @@ import { PickingId } from '../../../../mol-geo/geometry/picking';
import { StructureGroup } from '../../../structure/units-visual';
import { getResidueLoci } from './common';
export * from './polymer/backbone-iterator';
export * from './polymer/backbone';
export * from './polymer/gap-iterator';
export * from './polymer/trace-iterator';
export * from './polymer/curve-segment';

View File

@@ -1,134 +0,0 @@
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Unit, Structure, StructureElement, ElementIndex, ResidueIndex } from '../../../../../mol-model/structure';
import { Segmentation } from '../../../../../mol-data/int';
import { Iterator } from '../../../../../mol-data/iterator';
import { SortedRanges } from '../../../../../mol-data/int/sorted-ranges';
import { getPolymerRanges } from '../polymer';
/** Iterates over consecutive pairs of residues/coarse elements in polymers */
export function PolymerBackboneIterator(structure: Structure, unit: Unit): Iterator<PolymerBackbonePair> {
switch (unit.kind) {
case Unit.Kind.Atomic: return new AtomicPolymerBackboneIterator(structure, unit);
case Unit.Kind.Spheres:
case Unit.Kind.Gaussians:
return new CoarsePolymerBackboneIterator(structure, unit);
}
}
interface PolymerBackbonePair {
centerA: StructureElement.Location
centerB: StructureElement.Location
}
function createPolymerBackbonePair (structure: Structure, unit: Unit) {
return {
centerA: StructureElement.Location.create(structure, unit),
centerB: StructureElement.Location.create(structure, unit),
};
}
const enum AtomicPolymerBackboneIteratorState { nextPolymer, firstResidue, nextResidue, cycle }
export class AtomicPolymerBackboneIterator implements Iterator<PolymerBackbonePair> {
private traceElementIndex: ArrayLike<ElementIndex>
private value: PolymerBackbonePair
private polymerIt: SortedRanges.Iterator<ElementIndex, ResidueIndex>
private residueIt: Segmentation.SegmentIterator<ResidueIndex>
private state: AtomicPolymerBackboneIteratorState = AtomicPolymerBackboneIteratorState.nextPolymer
private residueSegment: Segmentation.Segment<ResidueIndex>
hasNext: boolean = false;
move() {
if (this.state === AtomicPolymerBackboneIteratorState.nextPolymer) {
while (this.polymerIt.hasNext) {
this.residueIt.setSegment(this.polymerIt.move());
if (this.residueIt.hasNext) {
this.residueSegment = this.residueIt.move();
this.value.centerB.element = this.traceElementIndex[this.residueSegment.index];
this.state = AtomicPolymerBackboneIteratorState.nextResidue;
break;
}
}
}
if (this.state === AtomicPolymerBackboneIteratorState.nextResidue) {
this.residueSegment = this.residueIt.move();
this.value.centerA.element = this.value.centerB.element;
this.value.centerB.element = this.traceElementIndex[this.residueSegment.index];
if (!this.residueIt.hasNext) {
if (this.unit.model.atomicRanges.cyclicPolymerMap.has(this.residueSegment.index)) {
this.state = AtomicPolymerBackboneIteratorState.cycle;
} else {
// TODO need to advance to a polymer that has two or more residues (can't assume it has)
this.state = AtomicPolymerBackboneIteratorState.nextPolymer;
}
}
} else if (this.state === AtomicPolymerBackboneIteratorState.cycle) {
const { cyclicPolymerMap } = this.unit.model.atomicRanges;
this.value.centerA.element = this.value.centerB.element;
this.value.centerB.element = this.traceElementIndex[cyclicPolymerMap.get(this.residueSegment.index)!];
// TODO need to advance to a polymer that has two or more residues (can't assume it has)
this.state = AtomicPolymerBackboneIteratorState.nextPolymer;
}
this.hasNext = this.residueIt.hasNext || this.polymerIt.hasNext || this.state === AtomicPolymerBackboneIteratorState.cycle;
return this.value;
}
constructor(structure: Structure, private unit: Unit.Atomic) {
this.traceElementIndex = unit.model.atomicHierarchy.derived.residue.traceElementIndex as ArrayLike<ElementIndex>; // can assume it won't be -1 for polymer residues
this.polymerIt = SortedRanges.transientSegments(getPolymerRanges(unit), unit.elements);
this.residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
this.value = createPolymerBackbonePair(structure, unit);
this.hasNext = this.residueIt.hasNext && this.polymerIt.hasNext;
}
}
const enum CoarsePolymerBackboneIteratorState { nextPolymer, nextElement }
export class CoarsePolymerBackboneIterator implements Iterator<PolymerBackbonePair> {
private value: PolymerBackbonePair
private polymerIt: SortedRanges.Iterator<ElementIndex, ResidueIndex>
private polymerSegment: Segmentation.Segment<ResidueIndex>
private state: CoarsePolymerBackboneIteratorState = CoarsePolymerBackboneIteratorState.nextPolymer
private elementIndex: number
hasNext: boolean = false;
move() {
if (this.state === CoarsePolymerBackboneIteratorState.nextPolymer) {
if (this.polymerIt.hasNext) {
this.polymerSegment = this.polymerIt.move();
this.elementIndex = this.polymerSegment.start;
if (this.elementIndex + 1 < this.polymerSegment.end) {
this.value.centerB.element = this.unit.elements[this.elementIndex];
this.state = CoarsePolymerBackboneIteratorState.nextElement;
} else {
this.state = CoarsePolymerBackboneIteratorState.nextPolymer;
}
}
}
if (this.state === CoarsePolymerBackboneIteratorState.nextElement) {
this.elementIndex += 1;
this.value.centerA.element = this.value.centerB.element;
this.value.centerB.element = this.unit.elements[this.elementIndex];
if (this.elementIndex + 1 >= this.polymerSegment.end) {
this.state = CoarsePolymerBackboneIteratorState.nextPolymer;
}
}
this.hasNext = this.elementIndex + 1 < this.polymerSegment.end || this.polymerIt.hasNext;
return this.value;
}
constructor(structure: Structure, private unit: Unit.Spheres | Unit.Gaussians) {
this.polymerIt = SortedRanges.transientSegments(getPolymerRanges(unit), unit.elements);
this.value = createPolymerBackbonePair(structure, unit);
this.hasNext = this.polymerIt.hasNext;
}
}

View File

@@ -0,0 +1,126 @@
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Segmentation } from '../../../../../mol-data/int';
import { SortedRanges } from '../../../../../mol-data/int/sorted-ranges';
import { ElementIndex, ResidueIndex, Unit } from '../../../../../mol-model/structure';
import { MoleculeType } from '../../../../../mol-model/structure/model/types';
import { getPolymerRanges } from '../polymer';
export type PolymerBackboneLinkCallback = (indexA: ElementIndex, indexB: ElementIndex, groupA: number, groupB: number, moleculeType: MoleculeType) => void
export function eachPolymerBackboneLink(unit: Unit, callback: PolymerBackboneLinkCallback) {
switch (unit.kind) {
case Unit.Kind.Atomic: return eachAtomicPolymerBackboneLink(unit, callback);
case Unit.Kind.Spheres:
case Unit.Kind.Gaussians:
return eachCoarsePolymerBackboneLink(unit, callback);
}
}
function eachAtomicPolymerBackboneLink(unit: Unit.Atomic, callback: PolymerBackboneLinkCallback) {
const cyclicPolymerMap = unit.model.atomicRanges.cyclicPolymerMap;
const polymerIt = SortedRanges.transientSegments(getPolymerRanges(unit), unit.elements);
const residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
const traceElementIndex = unit.model.atomicHierarchy.derived.residue.traceElementIndex as ArrayLike<ElementIndex>; // can assume it won't be -1 for polymer residues
const { moleculeType } = unit.model.atomicHierarchy.derived.residue;
let indexA = -1 as ResidueIndex;
let indexB = -1 as ResidueIndex;
let isFirst = true;
let firstGroup = -1;
let i = 0;
while (polymerIt.hasNext) {
isFirst = true;
firstGroup = i;
residueIt.setSegment(polymerIt.move());
while (residueIt.hasNext) {
if (isFirst) {
const index_1 = residueIt.move().index;
++i;
if (!residueIt.hasNext)
continue;
isFirst = false;
indexB = index_1;
}
const index = residueIt.move().index;
indexA = indexB;
indexB = index;
callback(traceElementIndex[indexA], traceElementIndex[indexB], i - 1, i, moleculeType[indexA]);
++i;
}
if (cyclicPolymerMap.has(indexB)) {
indexA = indexB;
indexB = cyclicPolymerMap.get(indexA)!;
callback(traceElementIndex[indexA], traceElementIndex[indexB], i - 1, firstGroup, moleculeType[indexA]);
}
}
}
function eachCoarsePolymerBackboneLink(unit: Unit.Spheres | Unit.Gaussians, callback: PolymerBackboneLinkCallback) {
const polymerIt = SortedRanges.transientSegments(getPolymerRanges(unit), unit.elements);
const { elements } = unit;
let isFirst = true;
let i = 0;
while (polymerIt.hasNext) {
isFirst = true;
const _a = polymerIt.move(), start = _a.start, end = _a.end;
for (let j = start, jl = end; j < jl; ++j) {
if (isFirst) {
++j;
++i;
if (j > jl)
continue;
isFirst = false;
}
callback(elements[j - 1], elements[j], i - 1, i, 0 /* Unknown */);
++i;
}
}
}
//
export type PolymerBackboneElementCallback = (index: ElementIndex, group: number) => void
export function eachPolymerBackboneElement(unit: Unit, callback: PolymerBackboneElementCallback) {
switch (unit.kind) {
case Unit.Kind.Atomic: return eachAtomicPolymerBackboneElement(unit, callback);
case Unit.Kind.Spheres:
case Unit.Kind.Gaussians:
return eachCoarsePolymerBackboneElement(unit, callback);
}
}
export function eachAtomicPolymerBackboneElement(unit: Unit.Atomic, callback: PolymerBackboneElementCallback) {
const polymerIt = SortedRanges.transientSegments(getPolymerRanges(unit), unit.elements);
const residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
const traceElementIndex = unit.model.atomicHierarchy.derived.residue.traceElementIndex as ArrayLike<ElementIndex>; // can assume it won't be -1 for polymer residues
let i = 0;
while (polymerIt.hasNext) {
residueIt.setSegment(polymerIt.move());
while (residueIt.hasNext) {
const index = residueIt.move().index;
callback(traceElementIndex[index], i);
++i;
}
}
}
function eachCoarsePolymerBackboneElement(unit: Unit.Spheres | Unit.Gaussians, callback: PolymerBackboneElementCallback) {
const polymerIt = SortedRanges.transientSegments(getPolymerRanges(unit), unit.elements);
const { elements } = unit;
let i = 0;
while (polymerIt.hasNext) {
const _a = polymerIt.move(), start = _a.start, end = _a.end;
for (let j = start, jl = end; j < jl; ++j) {
callback(elements[j], i);
++i;
}
}
}

View File

@@ -1,12 +1,12 @@
/**
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Vec3 } from '../../../../../mol-math/linear-algebra';
import { NumberArray } from '../../../../../mol-util/type-helpers';
import { lerp } from '../../../../../mol-math/interpolate';
import { lerp, smoothstep } from '../../../../../mol-math/interpolate';
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
const v3fromArray = Vec3.fromArray;
@@ -19,6 +19,8 @@ const v3copy = Vec3.copy;
const v3cross = Vec3.cross;
const v3orthogonalize = Vec3.orthogonalize;
const v3matchDirection = Vec3.matchDirection;
const v3scale = Vec3.scale;
const v3add = Vec3.add;
export interface CurveSegmentState {
curvePoints: NumberArray,
@@ -92,6 +94,7 @@ const tangentVec = Vec3();
const normalVec = Vec3();
const binormalVec = Vec3();
const prevNormal = Vec3();
const nextNormal = Vec3();
const firstTangentVec = Vec3();
const lastTangentVec = Vec3();
const firstNormalVec = Vec3();
@@ -116,8 +119,10 @@ export function interpolateNormals(state: CurveSegmentState, controls: CurveSegm
v3copy(prevNormal, firstNormalVec);
const n1 = n - 1;
for (let i = 0; i < n; ++i) {
const t = i === 0 ? 0 : 1 / (n - i);
const j = smoothstep(0, n1, i) * n1;
const t = i === 0 ? 0 : 1 / (n - j);
v3fromArray(tangentVec, tangentVectors, i * 3);
@@ -129,6 +134,19 @@ export function interpolateNormals(state: CurveSegmentState, controls: CurveSegm
v3normalize(binormalVec, v3cross(binormalVec, tangentVec, normalVec));
v3toArray(binormalVec, binormalVectors, i * 3);
}
for (let i = 1; i < n1; ++i) {
v3fromArray(prevNormal, normalVectors, (i - 1) * 3);
v3fromArray(normalVec, normalVectors, i * 3);
v3fromArray(nextNormal, normalVectors, (i + 1) * 3);
v3scale(normalVec, v3add(normalVec, prevNormal, v3add(normalVec, nextNormal, normalVec)), 1 / 3);
v3toArray(normalVec, normalVectors, i * 3);
v3fromArray(tangentVec, tangentVectors, i * 3);
v3normalize(binormalVec, v3cross(binormalVec, tangentVec, normalVec));
v3toArray(binormalVec, binormalVectors, i * 3);
}
}
export function interpolateSizes(state: CurveSegmentState, w0: number, w1: number, w2: number, h0: number, h1: number, h2: number, shift: number) {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -14,14 +14,31 @@ import { CoarseSphereConformation, CoarseGaussianConformation } from '../../../.
import { getPolymerRanges } from '../polymer';
import { AtomicConformation } from '../../../../../mol-model/structure/model/properties/atomic';
import { SecondaryStructureProvider } from '../../../../../mol-model-props/computed/secondary-structure';
import { HelixOrientationProvider } from '../../../../../mol-model-props/computed/helix-orientation';
import { SecondaryStructure } from '../../../../../mol-model/structure/model/properties/seconday-structure';
function isHelixSS(ss: SecondaryStructureType.Flag) {
return SecondaryStructureType.is(ss, SecondaryStructureType.Flag.Helix);
}
function isSheetSS(ss: SecondaryStructureType.Flag) {
return SecondaryStructureType.is(ss, SecondaryStructureType.Flag.Beta);
}
//
type PolymerTraceIteratorOptions = {
ignoreSecondaryStructure?: boolean,
useHelixOrientation?: boolean
}
/**
* Iterates over individual residues/coarse elements in polymers of a unit while
* providing information about the neighbourhood in the underlying model for drawing splines
*/
export function PolymerTraceIterator(unit: Unit, structure: Structure, ignoreSecondaryStructure = false): Iterator<PolymerTraceElement> {
export function PolymerTraceIterator(unit: Unit, structure: Structure, options: PolymerTraceIteratorOptions = {}): Iterator<PolymerTraceElement> {
switch (unit.kind) {
case Unit.Kind.Atomic: return new AtomicPolymerTraceIterator(unit, structure, ignoreSecondaryStructure);
case Unit.Kind.Atomic: return new AtomicPolymerTraceIterator(unit, structure, options);
case Unit.Kind.Spheres:
case Unit.Kind.Gaussians:
return new CoarsePolymerTraceIterator(unit, structure);
@@ -91,6 +108,8 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
private directionToElementIndex: ArrayLike<ElementIndex | -1>
private moleculeType: ArrayLike<MoleculeType>
private atomicConformation: AtomicConformation
private secondaryStructure: SecondaryStructure | undefined
private helixOrientationCenters: ArrayLike<number> | undefined
private p0 = Vec3()
private p1 = Vec3()
@@ -107,7 +126,7 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
hasNext: boolean = false;
private pos(target: Vec3, index: number) {
private atomicPos(target: Vec3, index: ElementIndex | -1) {
if (index !== -1) {
target[0] = this.atomicConformation.x[index];
target[1] = this.atomicConformation.y[index];
@@ -115,6 +134,15 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
}
}
private pos(target: Vec3, residueIndex: ResidueIndex, ss: SecondaryStructureType.Flag) {
const index = this.traceElementIndex[residueIndex];
if (this.helixOrientationCenters && isHelixSS(ss)) {
Vec3.fromArray(target, this.helixOrientationCenters, residueIndex * 3);
} else {
this.atomicPos(target, index);
}
}
private updateResidueSegmentRange(polymerSegment: Segmentation.Segment<number>) {
const { index } = this.residueAtomSegments;
this.residueSegmentMin = index[this.polymerRanges[polymerSegment.index * 2]];
@@ -140,23 +168,31 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
return residueIndex as ResidueIndex;
}
private getSecStruc: (residueIndex: ResidueIndex) => SecondaryStructureType.Flag
private getSecStruc(residueIndex: ResidueIndex): SecondaryStructureType.Flag {
if (this.secondaryStructure) {
const { type, getIndex } = this.secondaryStructure;
const ss = type[getIndex(residueIndex)];
// normalize helix-type
return isHelixSS(ss) ? SecondaryStructureType.Flag.Helix : ss;
} else {
return SecStrucTypeNA;
}
}
private setControlPoint(out: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, residueIndex: ResidueIndex) {
const ss = this.getSecStruc(residueIndex);
if (SecondaryStructureType.is(ss, SecondaryStructureType.Flag.Beta)) {
private setControlPoint(out: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, ss: SecondaryStructureType.Flag) {
if (isSheetSS(ss) || (this.helixOrientationCenters && isHelixSS(ss))) {
Vec3.scale(out, Vec3.add(out, p1, Vec3.add(out, p3, Vec3.add(out, p2, p2))), 1 / 4);
} else {
Vec3.copy(out, p2);
}
}
private setFromToVector(out: Vec3, residueIndex: ResidueIndex) {
if (this.value.isCoarseBackbone) {
private setFromToVector(out: Vec3, residueIndex: ResidueIndex, ss: SecondaryStructureType.Flag) {
if (this.value.isCoarseBackbone || (this.helixOrientationCenters && isHelixSS(ss))) {
Vec3.set(out, 1, 0, 0);
} else {
this.pos(tmpVecA, this.directionFromElementIndex[residueIndex]);
this.pos(tmpVecB, this.directionToElementIndex[residueIndex]);
this.atomicPos(tmpVecA, this.directionFromElementIndex[residueIndex]);
this.atomicPos(tmpVecB, this.directionToElementIndex[residueIndex]);
Vec3.sub(out, tmpVecB, tmpVecA);
}
}
@@ -203,7 +239,7 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
this.prevCoarseBackbone = this.currCoarseBackbone;
this.currCoarseBackbone = this.nextCoarseBackbone;
this.nextCoarseBackbone = residueIndex === residueIndexNext1 ? false : (this.directionFromElementIndex[residueIndexNext1] === -1 || this.directionToElementIndex[residueIndexNext1] === -1);
this.nextCoarseBackbone = this.directionFromElementIndex[residueIndexNext1] === -1 || this.directionToElementIndex[residueIndexNext1] === -1;
value.secStrucType = this.currSecStrucType;
value.secStrucFirst = this.prevSecStrucType !== this.currSecStrucType;
@@ -214,7 +250,6 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
value.first = residueIndex === this.residueSegmentMin;
value.last = residueIndex === this.residueSegmentMax;
value.moleculeType = this.moleculeType[residueIndex];
value.isCoarseBackbone = this.directionFromElementIndex[residueIndex] === -1 || this.directionToElementIndex[residueIndex] === -1;
value.initial = residueIndex === residueIndexPrev1;
value.final = residueIndex === residueIndexNext1;
@@ -223,53 +258,132 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
value.center.element = this.traceElementIndex[residueIndex];
value.centerNext.element = this.traceElementIndex[residueIndexNext1];
this.pos(this.p0, this.traceElementIndex[residueIndexPrev3]);
this.pos(this.p1, this.traceElementIndex[residueIndexPrev2]);
this.pos(this.p2, this.traceElementIndex[residueIndexPrev1]);
this.pos(this.p3, this.traceElementIndex[residueIndex]);
this.pos(this.p4, this.traceElementIndex[residueIndexNext1]);
this.pos(this.p5, this.traceElementIndex[residueIndexNext2]);
this.pos(this.p6, this.traceElementIndex[residueIndexNext3]);
const ssPrev3 = this.getSecStruc(residueIndexPrev3);
const ssPrev2 = this.getSecStruc(residueIndexPrev2);
const ssPrev1 = this.getSecStruc(residueIndexPrev1);
const ss = this.getSecStruc(residueIndex);
const ssNext1 = this.getSecStruc(residueIndexNext1);
const ssNext2 = this.getSecStruc(residueIndexNext2);
const ssNext3 = this.getSecStruc(residueIndexNext3);
this.setFromToVector(this.d01, residueIndexPrev1);
this.setFromToVector(this.d12, residueIndex);
this.setFromToVector(this.d23, residueIndexNext1);
this.setFromToVector(this.d34, residueIndexNext2);
this.pos(this.p0, residueIndexPrev3, ssPrev3);
this.pos(this.p1, residueIndexPrev2, ssPrev2);
this.pos(this.p2, residueIndexPrev1, ssPrev1);
this.pos(this.p3, residueIndex, ss);
this.pos(this.p4, residueIndexNext1, ssNext1);
this.pos(this.p5, residueIndexNext2, ssNext2);
this.pos(this.p6, residueIndexNext3, ssNext3);
const isHelixPrev3 = isHelixSS(ssPrev3);
const isHelixPrev2 = isHelixSS(ssPrev2);
const isHelixPrev1 = isHelixSS(ssPrev1);
const isHelix = isHelixSS(ss);
const isHelixNext1 = isHelixSS(ssNext1);
const isHelixNext2 = isHelixSS(ssNext2);
const isHelixNext3 = isHelixSS(ssNext3);
// handle positions for tubular helices
if (this.helixOrientationCenters) {
if (isHelix !== isHelixPrev1) {
if (isHelix) {
Vec3.copy(this.p0, this.p3);
Vec3.copy(this.p1, this.p3);
Vec3.copy(this.p2, this.p3);
} else if(isHelixPrev1) {
Vec3.scale(tmpDir, Vec3.sub(tmpDir, this.p2, this.p3), 2);
Vec3.add(this.p2, this.p3, tmpDir);
Vec3.add(this.p1, this.p2, tmpDir);
Vec3.add(this.p0, this.p1, tmpDir);
}
} else if (isHelix !== isHelixPrev2) {
if (isHelix) {
Vec3.copy(this.p0, this.p2);
Vec3.copy(this.p1, this.p2);
} else if(isHelixPrev2) {
Vec3.scale(tmpDir, Vec3.sub(tmpDir, this.p1, this.p2), 2);
Vec3.add(this.p1, this.p2, tmpDir);
Vec3.add(this.p0, this.p1, tmpDir);
}
} else if (isHelix !== isHelixPrev3) {
if (isHelix) {
Vec3.copy(this.p0, this.p1);
} else if(isHelixPrev3) {
Vec3.scale(tmpDir, Vec3.sub(tmpDir, this.p0, this.p1), 2);
Vec3.add(this.p0, this.p1, tmpDir);
}
}
if (isHelix !== isHelixNext1) {
if (isHelix) {
Vec3.copy(this.p4, this.p3);
Vec3.copy(this.p5, this.p3);
Vec3.copy(this.p6, this.p3);
} else if(isHelixNext1) {
Vec3.scale(tmpDir, Vec3.sub(tmpDir, this.p4, this.p3), 2);
Vec3.add(this.p4, this.p3, tmpDir);
Vec3.add(this.p5, this.p4, tmpDir);
Vec3.add(this.p6, this.p5, tmpDir);
}
} else if (isHelix !== isHelixNext2) {
if (isHelix) {
Vec3.copy(this.p5, this.p4);
Vec3.copy(this.p6, this.p4);
} else if(isHelixNext2) {
Vec3.scale(tmpDir, Vec3.sub(tmpDir, this.p5, this.p4), 2);
Vec3.add(this.p5, this.p4, tmpDir);
Vec3.add(this.p6, this.p5, tmpDir);
}
} else if (isHelix !== isHelixNext3) {
if (isHelix) {
Vec3.copy(this.p6, this.p5);
} else if(isHelixNext3) {
Vec3.scale(tmpDir, Vec3.sub(tmpDir, this.p6, this.p5), 2);
Vec3.add(this.p6, this.p5, tmpDir);
}
}
}
this.setFromToVector(this.d01, residueIndexPrev1, ssPrev1);
this.setFromToVector(this.d12, residueIndex, ss);
this.setFromToVector(this.d23, residueIndexNext1, ssNext1);
this.setFromToVector(this.d34, residueIndexNext2, ssNext2);
const helixFlag = isHelix && this.helixOrientationCenters;
// extend termini
const f = 0.5;
if (residueIndex === residueIndexPrev1) {
Vec3.scale(tmpDir, Vec3.sub(tmpDir, this.p3, this.p4), f);
const f = 1.5;
if (residueIndex === residueIndexPrev1 || (ss !== ssPrev1 && helixFlag)) {
Vec3.setMagnitude(tmpDir, Vec3.sub(tmpDir, this.p3, this.p4), f);
Vec3.add(this.p2, this.p3, tmpDir);
Vec3.add(this.p1, this.p2, tmpDir);
Vec3.add(this.p0, this.p1, tmpDir);
} else if (residueIndexPrev1 === residueIndexPrev2) {
Vec3.scale(tmpDir, Vec3.sub(tmpDir, this.p2, this.p3), f);
} else if (residueIndexPrev1 === residueIndexPrev2 || (ss !== ssPrev2 && helixFlag)) {
Vec3.setMagnitude(tmpDir, Vec3.sub(tmpDir, this.p2, this.p3), f);
Vec3.add(this.p1, this.p2, tmpDir);
Vec3.add(this.p0, this.p1, tmpDir);
} else if (residueIndexPrev2 === residueIndexPrev3) {
Vec3.scale(tmpDir, Vec3.sub(tmpDir, this.p1, this.p2), f);
} else if (residueIndexPrev2 === residueIndexPrev3 || (ss !== ssPrev3 && helixFlag)) {
Vec3.setMagnitude(tmpDir, Vec3.sub(tmpDir, this.p1, this.p2), f);
Vec3.add(this.p0, this.p1, tmpDir);
}
if (residueIndex === residueIndexNext1) {
Vec3.scale(tmpDir, Vec3.sub(tmpDir, this.p3, this.p2), f);
if (residueIndex === residueIndexNext1 || (ss !== ssNext1 && helixFlag)) {
Vec3.setMagnitude(tmpDir, Vec3.sub(tmpDir, this.p3, this.p2), f);
Vec3.add(this.p4, this.p3, tmpDir);
Vec3.add(this.p5, this.p4, tmpDir);
Vec3.add(this.p6, this.p5, tmpDir);
} else if (residueIndexNext1 === residueIndexNext2) {
Vec3.scale(tmpDir, Vec3.sub(tmpDir, this.p4, this.p3), f);
} else if (residueIndexNext1 === residueIndexNext2 || (ss !== ssNext2 && helixFlag)) {
Vec3.setMagnitude(tmpDir, Vec3.sub(tmpDir, this.p4, this.p3), f);
Vec3.add(this.p5, this.p4, tmpDir);
Vec3.add(this.p6, this.p5, tmpDir);
} else if (residueIndexNext2 === residueIndexNext3) {
Vec3.scale(tmpDir, Vec3.sub(tmpDir, this.p5, this.p4), f);
} else if (residueIndexNext2 === residueIndexNext3 || (ss !== ssNext3 && helixFlag)) {
Vec3.setMagnitude(tmpDir, Vec3.sub(tmpDir, this.p5, this.p4), f);
Vec3.add(this.p6, this.p5, tmpDir);
}
this.setControlPoint(value.p0, this.p0, this.p1, this.p2, residueIndexPrev2);
this.setControlPoint(value.p1, this.p1, this.p2, this.p3, residueIndexPrev1);
this.setControlPoint(value.p2, this.p2, this.p3, this.p4, residueIndex);
this.setControlPoint(value.p3, this.p3, this.p4, this.p5, residueIndexNext1);
this.setControlPoint(value.p4, this.p4, this.p5, this.p6, residueIndexNext2);
this.setControlPoint(value.p0, this.p0, this.p1, this.p2, ssPrev2);
this.setControlPoint(value.p1, this.p1, this.p2, this.p3, ssPrev1);
this.setControlPoint(value.p2, this.p2, this.p3, this.p4, ss);
this.setControlPoint(value.p3, this.p3, this.p4, this.p5, ssNext1);
this.setControlPoint(value.p4, this.p4, this.p5, this.p6, ssNext2);
this.setDirection(value.d12, this.d01, this.d12, this.d23);
this.setDirection(value.d23, this.d12, this.d23, this.d34);
@@ -284,7 +398,7 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
return this.value;
}
constructor(private unit: Unit.Atomic, structure: Structure, ignoreSecondaryStructure = false) {
constructor(private unit: Unit.Atomic, structure: Structure, options: PolymerTraceIteratorOptions = {}) {
this.atomicConformation = unit.model.atomicConformation;
this.residueAtomSegments = unit.model.atomicHierarchy.residueAtomSegments;
this.polymerRanges = unit.model.atomicRanges.polymerRanges;
@@ -298,12 +412,15 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
this.value = createPolymerTraceElement(structure, unit);
this.hasNext = this.residueIt.hasNext && this.polymerIt.hasNext;
const secondaryStructure = !ignoreSecondaryStructure && SecondaryStructureProvider.get(structure).value?.get(unit.invariantId);
if (secondaryStructure) {
const { type, getIndex } = secondaryStructure;
this.getSecStruc = (residueIndex: ResidueIndex) => type[getIndex(residueIndex as ResidueIndex)];
} else {
this.getSecStruc = (residueIndex: ResidueIndex) => SecStrucTypeNA;
if (!options.ignoreSecondaryStructure) {
this.secondaryStructure = SecondaryStructureProvider.get(structure).value?.get(unit.invariantId);
}
if (options.useHelixOrientation) {
const helixOrientation = HelixOrientationProvider.get(unit.model).value;
if (!helixOrientation) throw new Error('missing helix-orientation');
this.helixOrientationCenters = helixOrientation.centers;
}
}
}

View File

@@ -123,6 +123,6 @@ export const SecondaryStructureColorThemeProvider: ColorTheme.Provider<Secondary
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure,
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? SecondaryStructureProvider.attach(ctx, data.structure, void 0, true) : Promise.resolve(),
detach: (data) => data.structure && data.structure.customPropertyDescriptors.reference(SecondaryStructureProvider.descriptor, false)
detach: (data) => data.structure && SecondaryStructureProvider.ref(data.structure, false)
}
};

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -105,6 +105,29 @@ export function arraySetRemove<T>(xs: T[], x: T) {
return true;
}
/**
* Caution, O(n^2) complexity. Only use for small input sizes.
* For larger inputs consider using `SortedArray`.
*/
export function arrayAreIntersecting<T>(xs: T[], ys: T[]) {
for (let i = 0, il = xs.length; i < il; ++i) {
if (ys.includes(xs[i])) return true;
}
return false;
}
/**
* Caution, O(n^2) complexity. Only use for small input sizes.
* For larger inputs consider using `SortedArray`.
*/
export function arrayIntersectionSize<T>(xs: T[], ys: T[]) {
let count = 0;
for (let i = 0, il = xs.length; i < il; ++i) {
if (ys.includes(xs[i])) count += 1;
}
return count;
}
export function arrayEqual<T>(xs?: ArrayLike<T>, ys?: ArrayLike<T>) {
if (!xs || xs.length === 0) return !ys || ys.length === 0;
if (!ys) return false;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019 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>
*/
@@ -32,4 +32,16 @@ namespace PixelData {
}
return pixelData;
}
/** to undo pre-multiplied alpha */
export function divideByAlpha(pixelData: PixelData): PixelData {
const { array } = pixelData;
for (let i = 0, il = array.length; i < il; i += 4) {
const a = array[i + 3] / 255;
array[i] /= a;
array[i + 1] /= a;
array[i + 2] /= a;
}
return pixelData;
}
}