Compare commits

...

13 Commits

Author SHA1 Message Date
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
39 changed files with 323 additions and 122 deletions

View File

@@ -3,8 +3,16 @@ 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]
## [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.0.7",
"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.0.7",
"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';
@@ -43,17 +44,18 @@ export class GeometryControls extends PluginComponent {
const renderObjects = this.plugin.canvas3d?.getRenderObjects()!;
const filename = this.getFilename();
let renderObjectExporter: ObjExporter | GlbExporter | StlExporter;
const boundingBox = Box3D.fromSphere3D(Box3D(), this.plugin.canvas3d?.boundingSphereVisible!);
let renderObjectExporter: GlbExporter | ObjExporter | StlExporter;
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;
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,11 +254,12 @@ 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);
}
}
@@ -334,7 +337,11 @@ export class GlbExporter extends MeshExporter<GlbData> {
return new Blob([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

@@ -6,6 +6,7 @@
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 +36,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;
@@ -163,6 +165,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
@@ -273,8 +276,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

@@ -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);
@@ -105,4 +108,12 @@ export class StlExporter extends MeshExporter<StlData> {
async getBlob(ctx: RuntimeContext) {
return new Blob([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

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

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

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

@@ -38,6 +38,8 @@ export function coordinatesFromDcd(dcdFile: DcdFile): Task<Coordinates> {
x: dcdFrame.x,
y: dcdFrame.y,
z: dcdFrame.z,
xyzOrdering: { isIdentity: true }
};
if (dcdFrame.cell) {

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

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

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

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

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

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

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

@@ -187,6 +187,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 ||

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