mirror of
https://github.com/molstar/molstar.git
synced 2026-06-07 07:04:22 +08:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
944d370c14 | ||
|
|
74f9aa6af6 | ||
|
|
c20c9c9917 | ||
|
|
4801435d72 | ||
|
|
33fd105ef7 | ||
|
|
3ea3fb8984 | ||
|
|
b4bbc544ca | ||
|
|
5f880e920b | ||
|
|
bcce801dd7 | ||
|
|
00f9dcee4a | ||
|
|
505af2bc96 | ||
|
|
c217aab5fc | ||
|
|
5d5fd0028f | ||
|
|
c88693dfdd | ||
|
|
0a16ec1bd2 |
28
package-lock.json
generated
28
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -1363,9 +1363,9 @@
|
||||
"integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "12.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.11.tgz",
|
||||
"integrity": "sha512-Otxmr2rrZLKRYIybtdG/sgeO+tHY20GxeDjcGmUnmmlCWyEnv2a2x1ZXBo3BTec4OiTXMQCiazB8NMBf0iRlFw=="
|
||||
"version": "12.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.12.tgz",
|
||||
"integrity": "sha512-KPYGmfD0/b1eXurQ59fXD1GBzhSQfz6/lKBxkaHX9dKTzjXbK68Zt7yGUxUsCS1jeTy/8aL+d9JEr+S54mpkWQ=="
|
||||
},
|
||||
"@types/node-fetch": {
|
||||
"version": "2.5.2",
|
||||
@@ -3051,13 +3051,13 @@
|
||||
}
|
||||
},
|
||||
"concurrently": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-4.1.2.tgz",
|
||||
"integrity": "sha512-Kim9SFrNr2jd8/0yNYqDTFALzUX1tvimmwFWxmp/D4mRI+kbqIIwE2RkBDrxS2ic25O1UgQMI5AtBqdtX3ynYg==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-5.0.0.tgz",
|
||||
"integrity": "sha512-1yDvK8mduTIdxIxV9C60KoiOySUl/lfekpdbI+U5GXaPrgdffEavFa9QZB3vh68oWOpbCC+TuvxXV9YRPMvUrA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^2.4.2",
|
||||
"date-fns": "^1.30.1",
|
||||
"date-fns": "^2.0.1",
|
||||
"lodash": "^4.17.15",
|
||||
"read-pkg": "^4.0.1",
|
||||
"rxjs": "^6.5.2",
|
||||
@@ -3099,9 +3099,9 @@
|
||||
}
|
||||
},
|
||||
"date-fns": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz",
|
||||
"integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==",
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.4.1.tgz",
|
||||
"integrity": "sha512-2RhmH/sjDSCYW2F3ZQxOUx/I7PvzXpi89aQL2d3OAxSTwLx6NilATeUbe0menFE3Lu5lFkOFci36ivimwYHHxw==",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
@@ -13832,9 +13832,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"typescript": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.3.tgz",
|
||||
"integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==",
|
||||
"version": "3.6.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz",
|
||||
"integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==",
|
||||
"dev": true
|
||||
},
|
||||
"uglify-js": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.2",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -65,7 +65,7 @@
|
||||
"devDependencies": {
|
||||
"benchmark": "^2.1.4",
|
||||
"circular-dependency-plugin": "^5.2.0",
|
||||
"concurrently": "^4.1.2",
|
||||
"concurrently": "^5.0.0",
|
||||
"cpx": "^1.5.0",
|
||||
"css-loader": "^3.2.0",
|
||||
"extra-watch-webpack-plugin": "^1.0.3",
|
||||
@@ -86,7 +86,7 @@
|
||||
"style-loader": "^1.0.0",
|
||||
"ts-jest": "^24.1.0",
|
||||
"tslint": "^5.20.0",
|
||||
"typescript": "^3.6.3",
|
||||
"typescript": "^3.6.4",
|
||||
"webpack": "^4.41.0",
|
||||
"webpack-cli": "^3.3.9"
|
||||
},
|
||||
@@ -96,7 +96,7 @@
|
||||
"@types/compression": "1.0.1",
|
||||
"@types/express": "^4.17.1",
|
||||
"@types/jest": "^24.0.18",
|
||||
"@types/node": "^12.7.11",
|
||||
"@types/node": "^12.7.12",
|
||||
"@types/node-fetch": "^2.5.2",
|
||||
"@types/react": "^16.9.5",
|
||||
"@types/react-dom": "^16.9.1",
|
||||
|
||||
@@ -32,6 +32,7 @@ import { readTexture } from '../mol-gl/compute/util';
|
||||
import { DrawPass } from './passes/draw';
|
||||
import { PickPass } from './passes/pick';
|
||||
import { Task } from '../mol-task';
|
||||
import { ImagePass, ImageProps } from './passes/image';
|
||||
|
||||
export const Canvas3DParams = {
|
||||
cameraMode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']]),
|
||||
@@ -74,6 +75,7 @@ interface Canvas3D {
|
||||
downloadScreenshot: () => void
|
||||
getPixelData: (variant: GraphicsRenderVariant) => PixelData
|
||||
setProps: (props: Partial<Canvas3DProps>) => void
|
||||
getImagePass: () => ImagePass
|
||||
|
||||
/** Returns a copy of the current Canvas3D instance props */
|
||||
readonly props: Readonly<Canvas3DProps>
|
||||
@@ -93,10 +95,11 @@ namespace Canvas3D {
|
||||
|
||||
export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}, runTask = DefaultRunTask) {
|
||||
const gl = getGLContext(canvas, {
|
||||
alpha: false,
|
||||
alpha: true,
|
||||
antialias: true,
|
||||
depth: true,
|
||||
preserveDrawingBuffer: true
|
||||
preserveDrawingBuffer: true,
|
||||
premultipliedAlpha: false,
|
||||
})
|
||||
if (gl === null) throw new Error('Could not create a WebGL rendering context')
|
||||
const input = InputObserver.fromElement(canvas)
|
||||
@@ -127,12 +130,12 @@ namespace Canvas3D {
|
||||
})
|
||||
|
||||
const controls = TrackballControls.create(input, camera, p.trackball)
|
||||
const renderer = Renderer.create(webgl, camera, p.renderer)
|
||||
const renderer = Renderer.create(webgl, p.renderer)
|
||||
const debugHelper = new BoundingSphereHelper(webgl, scene, p.debug);
|
||||
const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input);
|
||||
|
||||
const drawPass = new DrawPass(webgl, renderer, scene, debugHelper)
|
||||
const pickPass = new PickPass(webgl, renderer, scene, 0.5)
|
||||
const drawPass = new DrawPass(webgl, renderer, scene, camera, debugHelper)
|
||||
const pickPass = new PickPass(webgl, renderer, scene, camera, 0.5)
|
||||
const postprocessing = new PostprocessingPass(webgl, camera, drawPass, p.postprocessing)
|
||||
const multiSample = new MultiSamplePass(webgl, camera, drawPass, postprocessing, p.multiSample)
|
||||
|
||||
@@ -176,6 +179,7 @@ namespace Canvas3D {
|
||||
|
||||
let didRender = false
|
||||
controls.update(currentTime)
|
||||
Viewport.set(camera.viewport, 0, 0, width, height)
|
||||
const cameraChanged = camera.update()
|
||||
multiSample.update(force || cameraChanged, currentTime)
|
||||
|
||||
@@ -185,9 +189,9 @@ namespace Canvas3D {
|
||||
pickPass.render()
|
||||
break;
|
||||
case 'draw':
|
||||
renderer.setViewport(0, 0, width, height);
|
||||
renderer.setViewport(0, 0, width, height)
|
||||
if (multiSample.enabled) {
|
||||
multiSample.render()
|
||||
multiSample.render(true)
|
||||
} else {
|
||||
drawPass.render(!postprocessing.enabled)
|
||||
if (postprocessing.enabled) postprocessing.render(true)
|
||||
@@ -308,7 +312,7 @@ namespace Canvas3D {
|
||||
getLoci,
|
||||
|
||||
handleResize,
|
||||
resetCamera: (/*dir?: Vec3*/) => {
|
||||
resetCamera: () => {
|
||||
if (scene.isCommiting) {
|
||||
cameraResetRequested = true
|
||||
} else {
|
||||
@@ -347,6 +351,9 @@ namespace Canvas3D {
|
||||
if (props.debug) debugHelper.setProps(props.debug)
|
||||
requestDraw(true)
|
||||
},
|
||||
getImagePass: (props: Partial<ImageProps> = {}) => {
|
||||
return new ImagePass(webgl, renderer, scene, camera, debugHelper, props)
|
||||
},
|
||||
|
||||
get props() {
|
||||
return {
|
||||
|
||||
@@ -29,7 +29,7 @@ export const DefaultTrackballBindings = {
|
||||
dragFocusZoom: Binding(Trigger(B.Flag.Auxilary, M.create()), 'Focus and zoom the 3D scene by dragging using ${trigger}'),
|
||||
|
||||
scrollZoom: Binding(Trigger(B.Flag.Auxilary, M.create()), 'Zoom the 3D scene by scrolling using ${trigger}'),
|
||||
scrollFocus: Binding(Trigger(B.Flag.Auxilary, M.create({ shift: true })), 'Focus the 3D scene by dragging using ${trigger}'),
|
||||
scrollFocus: Binding(Trigger(B.Flag.Auxilary, M.create({ shift: true })), 'Focus the 3D scene by scrolling using ${trigger}'),
|
||||
scrollFocusZoom: Binding.Empty,
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import Renderer from '../../mol-gl/renderer';
|
||||
import Scene from '../../mol-gl/scene';
|
||||
import { BoundingSphereHelper } from '../helper/bounding-sphere-helper';
|
||||
import { createTexture, Texture } from '../../mol-gl/webgl/texture';
|
||||
import { Camera } from '../camera';
|
||||
|
||||
export class DrawPass {
|
||||
colorTarget: RenderTarget
|
||||
@@ -18,11 +19,11 @@ export class DrawPass {
|
||||
|
||||
private depthTarget: RenderTarget | null
|
||||
|
||||
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private debugHelper: BoundingSphereHelper) {
|
||||
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, private debugHelper: BoundingSphereHelper) {
|
||||
const { gl, extensions } = webgl
|
||||
const width = gl.drawingBufferWidth
|
||||
const height = gl.drawingBufferHeight
|
||||
this.colorTarget = createRenderTarget(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight)
|
||||
this.colorTarget = createRenderTarget(webgl, width, height)
|
||||
this.packedDepth = !extensions.depthTexture
|
||||
this.depthTarget = this.packedDepth ? createRenderTarget(webgl, width, height) : null
|
||||
this.depthTexture = this.depthTarget ? this.depthTarget.texture : createTexture(webgl, 'image-depth', 'depth', 'ushort', 'nearest')
|
||||
@@ -42,28 +43,28 @@ export class DrawPass {
|
||||
}
|
||||
|
||||
render(toDrawingBuffer: boolean) {
|
||||
const { webgl, renderer, scene, debugHelper, colorTarget, depthTarget } = this
|
||||
const { gl } = webgl
|
||||
const { webgl, renderer, scene, camera, debugHelper, colorTarget, depthTarget } = this
|
||||
if (toDrawingBuffer) {
|
||||
webgl.unbindFramebuffer()
|
||||
} else {
|
||||
colorTarget.bind()
|
||||
}
|
||||
renderer.setViewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight)
|
||||
renderer.render(scene, 'color', true)
|
||||
|
||||
renderer.setViewport(0, 0, colorTarget.width, colorTarget.height)
|
||||
renderer.render(scene, camera, 'color', true)
|
||||
if (debugHelper.isEnabled) {
|
||||
debugHelper.syncVisibility()
|
||||
renderer.render(debugHelper.scene, 'color', false)
|
||||
renderer.render(debugHelper.scene, camera, 'color', false)
|
||||
}
|
||||
|
||||
// do a depth pass if not rendering to drawing buffer and
|
||||
// extensions.depthTexture is unsupported (i.e. depthTarget is set)
|
||||
if (!toDrawingBuffer && depthTarget) {
|
||||
depthTarget.bind()
|
||||
renderer.render(scene, 'depth', true)
|
||||
renderer.render(scene, camera, 'depth', true)
|
||||
if (debugHelper.isEnabled) {
|
||||
debugHelper.syncVisibility()
|
||||
renderer.render(debugHelper.scene, 'depth', false)
|
||||
renderer.render(debugHelper.scene, camera, 'depth', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
91
src/mol-canvas3d/passes/image.ts
Normal file
91
src/mol-canvas3d/passes/image.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import Renderer from '../../mol-gl/renderer';
|
||||
import Scene from '../../mol-gl/scene';
|
||||
import { BoundingSphereHelper } from '../helper/bounding-sphere-helper';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { DrawPass } from './draw'
|
||||
import { PostprocessingPass, PostprocessingParams } from './postprocessing'
|
||||
import { MultiSamplePass, MultiSampleParams } from './multi-sample'
|
||||
import { Camera } from '../camera';
|
||||
import { Viewport } from '../camera/util';
|
||||
|
||||
export const ImageParams = {
|
||||
multiSample: PD.Group(MultiSampleParams),
|
||||
postprocessing: PD.Group(PostprocessingParams),
|
||||
}
|
||||
export type ImageProps = PD.Values<typeof ImageParams>
|
||||
|
||||
export class ImagePass {
|
||||
private _width = 1024
|
||||
private _height = 768
|
||||
private _camera = new Camera()
|
||||
|
||||
private _colorTarget: RenderTarget
|
||||
get colorTarget() { return this._colorTarget }
|
||||
|
||||
readonly drawPass: DrawPass
|
||||
private readonly postprocessing: PostprocessingPass
|
||||
private readonly multiSample: MultiSamplePass
|
||||
|
||||
get width() { return this._width }
|
||||
get height() { return this._height }
|
||||
|
||||
constructor(webgl: WebGLContext, private renderer: Renderer, scene: Scene, private camera: Camera, debugHelper: BoundingSphereHelper, props: Partial<ImageProps>) {
|
||||
const p = { ...PD.getDefaultValues(ImageParams), ...props }
|
||||
|
||||
this.drawPass = new DrawPass(webgl, renderer, scene, this._camera, debugHelper)
|
||||
this.postprocessing = new PostprocessingPass(webgl, this._camera, this.drawPass, p.postprocessing)
|
||||
this.multiSample = new MultiSamplePass(webgl, this._camera, this.drawPass, this.postprocessing, p.multiSample)
|
||||
|
||||
this.setSize(this._width, this._height)
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
this._width = width
|
||||
this._height = height
|
||||
|
||||
this.drawPass.setSize(width, height)
|
||||
this.postprocessing.setSize(width, height)
|
||||
this.multiSample.setSize(width, height)
|
||||
}
|
||||
|
||||
setProps(props: Partial<ImageProps> = {}) {
|
||||
if (props.postprocessing) this.postprocessing.setProps(props.postprocessing)
|
||||
if (props.multiSample) this.multiSample.setProps(props.multiSample)
|
||||
}
|
||||
|
||||
render() {
|
||||
Camera.copySnapshot(this._camera.state, this.camera.state)
|
||||
Viewport.set(this._camera.viewport, 0, 0, this._width, this._height)
|
||||
this._camera.update()
|
||||
|
||||
this.renderer.setViewport(0, 0, this._width, this._height);
|
||||
|
||||
if (this.multiSample.enabled) {
|
||||
this.multiSample.render(false)
|
||||
this._colorTarget = this.multiSample.colorTarget
|
||||
} else {
|
||||
this.drawPass.render(false)
|
||||
if (this.postprocessing.enabled) {
|
||||
this.postprocessing.render(false)
|
||||
this._colorTarget = this.postprocessing.target
|
||||
} else {
|
||||
this._colorTarget = this.drawPass.colorTarget
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getImageData(width: number, height: number) {
|
||||
this.setSize(width, height)
|
||||
this.render()
|
||||
const pd = this.colorTarget.getPixelData()
|
||||
return new ImageData(new Uint8ClampedArray(pd.array), pd.width, pd.height)
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,7 @@ export type MultiSampleProps = PD.Values<typeof MultiSampleParams>
|
||||
|
||||
export class MultiSamplePass {
|
||||
props: MultiSampleProps
|
||||
colorTarget: RenderTarget
|
||||
|
||||
private composeTarget: RenderTarget
|
||||
private holdTarget: RenderTarget
|
||||
@@ -65,6 +66,7 @@ export class MultiSamplePass {
|
||||
|
||||
constructor(private webgl: WebGLContext, private camera: Camera, private drawPass: DrawPass, private postprocessing: PostprocessingPass, props: Partial<MultiSampleProps>) {
|
||||
const { gl } = webgl
|
||||
this.colorTarget = createRenderTarget(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight)
|
||||
this.composeTarget = createRenderTarget(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight)
|
||||
this.holdTarget = createRenderTarget(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight)
|
||||
this.compose = getComposeRenderable(webgl, drawPass.colorTarget.texture)
|
||||
@@ -92,6 +94,7 @@ export class MultiSamplePass {
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
this.colorTarget.setSize(width, height)
|
||||
this.composeTarget.setSize(width, height)
|
||||
this.holdTarget.setSize(width, height)
|
||||
ValueCell.update(this.compose.values.uTexSize, Vec2.set(this.compose.values.uTexSize.ref.value, width, height))
|
||||
@@ -102,15 +105,15 @@ export class MultiSamplePass {
|
||||
if (props.sampleLevel !== undefined) this.props.sampleLevel = props.sampleLevel
|
||||
}
|
||||
|
||||
render() {
|
||||
render(toDrawingBuffer: boolean) {
|
||||
if (this.props.mode === 'temporal') {
|
||||
this.renderTemporalMultiSample()
|
||||
this.renderTemporalMultiSample(toDrawingBuffer)
|
||||
} else {
|
||||
this.renderMultiSample()
|
||||
this.renderMultiSample(toDrawingBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
private renderMultiSample() {
|
||||
private renderMultiSample(toDrawingBuffer: boolean) {
|
||||
const { camera, compose, composeTarget, drawPass, postprocessing, webgl } = this
|
||||
const { gl, state } = webgl
|
||||
|
||||
@@ -168,7 +171,11 @@ export class MultiSamplePass {
|
||||
ValueCell.update(compose.values.tColor, composeTarget.texture)
|
||||
compose.update()
|
||||
|
||||
webgl.unbindFramebuffer()
|
||||
if (toDrawingBuffer) {
|
||||
webgl.unbindFramebuffer()
|
||||
} else {
|
||||
this.colorTarget.bind()
|
||||
}
|
||||
gl.viewport(0, 0, width, height)
|
||||
state.disable(gl.BLEND)
|
||||
compose.render()
|
||||
@@ -177,7 +184,7 @@ export class MultiSamplePass {
|
||||
camera.update()
|
||||
}
|
||||
|
||||
private renderTemporalMultiSample() {
|
||||
private renderTemporalMultiSample(toDrawingBuffer: boolean) {
|
||||
const { camera, compose, composeTarget, holdTarget, postprocessing, drawPass, webgl } = this
|
||||
const { gl, state } = webgl
|
||||
|
||||
@@ -252,7 +259,11 @@ export class MultiSamplePass {
|
||||
ValueCell.update(compose.values.uWeight, 1.0)
|
||||
ValueCell.update(compose.values.tColor, composeTarget.texture)
|
||||
compose.update()
|
||||
webgl.unbindFramebuffer()
|
||||
if (toDrawingBuffer) {
|
||||
webgl.unbindFramebuffer()
|
||||
} else {
|
||||
this.colorTarget.bind()
|
||||
}
|
||||
gl.viewport(0, 0, width, height)
|
||||
state.disable(gl.BLEND)
|
||||
compose.render()
|
||||
@@ -261,7 +272,11 @@ export class MultiSamplePass {
|
||||
ValueCell.update(compose.values.uWeight, 1.0 - accumulationWeight)
|
||||
ValueCell.update(compose.values.tColor, holdTarget.texture)
|
||||
compose.update()
|
||||
webgl.unbindFramebuffer()
|
||||
if (toDrawingBuffer) {
|
||||
webgl.unbindFramebuffer()
|
||||
} else {
|
||||
this.colorTarget.bind()
|
||||
}
|
||||
gl.viewport(0, 0, width, height)
|
||||
if (accumulationWeight === 0) state.disable(gl.BLEND)
|
||||
else state.enable(gl.BLEND)
|
||||
|
||||
@@ -10,6 +10,7 @@ import Renderer from '../../mol-gl/renderer';
|
||||
import Scene from '../../mol-gl/scene';
|
||||
import { PickingId } from '../../mol-geo/geometry/picking';
|
||||
import { decodeFloatRGB } from '../../mol-util/float-packing';
|
||||
import { Camera } from '../camera';
|
||||
|
||||
export class PickPass {
|
||||
pickDirty = true
|
||||
@@ -26,7 +27,7 @@ export class PickPass {
|
||||
private pickWidth: number
|
||||
private pickHeight: number
|
||||
|
||||
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private pickBaseScale: number) {
|
||||
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, private pickBaseScale: number) {
|
||||
const { gl } = webgl
|
||||
const width = gl.drawingBufferWidth
|
||||
const height = gl.drawingBufferHeight
|
||||
@@ -64,14 +65,14 @@ export class PickPass {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { renderer, scene } = this
|
||||
const { renderer, scene, camera } = this
|
||||
renderer.setViewport(0, 0, this.pickWidth, this.pickHeight);
|
||||
this.objectPickTarget.bind();
|
||||
renderer.render(scene, 'pickObject', true);
|
||||
renderer.render(scene, camera, 'pickObject', true);
|
||||
this.instancePickTarget.bind();
|
||||
renderer.render(scene, 'pickInstance', true);
|
||||
renderer.render(scene, camera, 'pickInstance', true);
|
||||
this.groupPickTarget.bind();
|
||||
renderer.render(scene, 'pickGroup', true);
|
||||
renderer.render(scene, camera, 'pickGroup', true);
|
||||
|
||||
this.pickDirty = false
|
||||
}
|
||||
|
||||
@@ -4,16 +4,57 @@
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
/** resize canvas to container element */
|
||||
/** Set canvas size taking `devicePixelRatio` into account */
|
||||
export function setCanvasSize(canvas: HTMLCanvasElement, width: number, height: number) {
|
||||
canvas.width = window.devicePixelRatio * width
|
||||
canvas.height = window.devicePixelRatio * height
|
||||
Object.assign(canvas.style, { width: `${width}px`, height: `${height}px` })
|
||||
}
|
||||
|
||||
/** Resize canvas to container element taking `devicePixelRatio` into account */
|
||||
export function resizeCanvas (canvas: HTMLCanvasElement, container: Element) {
|
||||
let w = window.innerWidth
|
||||
let h = window.innerHeight
|
||||
let width = window.innerWidth
|
||||
let height = window.innerHeight
|
||||
if (container !== document.body) {
|
||||
let bounds = container.getBoundingClientRect()
|
||||
w = bounds.right - bounds.left
|
||||
h = bounds.bottom - bounds.top
|
||||
width = bounds.right - bounds.left
|
||||
height = bounds.bottom - bounds.top
|
||||
}
|
||||
canvas.width = window.devicePixelRatio * w
|
||||
canvas.height = window.devicePixelRatio * h
|
||||
Object.assign(canvas.style, { width: `${w}px`, height: `${h}px` })
|
||||
setCanvasSize(canvas, width, height)
|
||||
}
|
||||
|
||||
function _canvasToBlob(canvas: HTMLCanvasElement, callback: BlobCallback, type?: string, quality?: any) {
|
||||
const bin = atob(canvas.toDataURL(type, quality).split(',')[1])
|
||||
const len = bin.length
|
||||
const len32 = len >> 2
|
||||
const a8 = new Uint8Array(len)
|
||||
const a32 = new Uint32Array( a8.buffer, 0, len32 )
|
||||
|
||||
let j = 0
|
||||
for (let i = 0; i < len32; ++i) {
|
||||
a32[i] = bin.charCodeAt(j++) |
|
||||
bin.charCodeAt(j++) << 8 |
|
||||
bin.charCodeAt(j++) << 16 |
|
||||
bin.charCodeAt(j++) << 24
|
||||
}
|
||||
|
||||
let tailLength = len & 3;
|
||||
while (tailLength--) a8[j] = bin.charCodeAt(j++)
|
||||
|
||||
callback(new Blob([a8], { type: type || 'image/png' }));
|
||||
}
|
||||
|
||||
export async function canvasToBlob(canvas: HTMLCanvasElement, type?: string, quality?: any): Promise<Blob> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const callback = (blob: Blob | null) => {
|
||||
if (blob) resolve(blob)
|
||||
else reject('no blob returned')
|
||||
}
|
||||
|
||||
if (!HTMLCanvasElement.prototype.toBlob) {
|
||||
_canvasToBlob(canvas, callback, type, quality)
|
||||
} else {
|
||||
canvas.toBlob(callback, type, quality)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -30,7 +30,7 @@ function createRenderer(gl: WebGLRenderingContext) {
|
||||
const camera = new Camera({
|
||||
position: Vec3.create(0, 0, 50)
|
||||
})
|
||||
const renderer = Renderer.create(ctx, camera)
|
||||
const renderer = Renderer.create(ctx)
|
||||
return { ctx, camera, renderer }
|
||||
}
|
||||
|
||||
|
||||
@@ -168,6 +168,7 @@ export const GlobalUniformSchema = {
|
||||
uFogFar: UniformSpec('f'),
|
||||
uFogColor: UniformSpec('v3'),
|
||||
|
||||
uTransparentBackground: UniformSpec('i'),
|
||||
uPickingAlphaThreshold: UniformSpec('f'),
|
||||
uInteriorDarkening: UniformSpec('f'),
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ interface Renderer {
|
||||
readonly props: Readonly<RendererProps>
|
||||
|
||||
clear: () => void
|
||||
render: (scene: Scene, variant: GraphicsRenderVariant, clear: boolean) => void
|
||||
render: (scene: Scene, camera: Camera, variant: GraphicsRenderVariant, clear: boolean) => void
|
||||
setProps: (props: Partial<RendererProps>) => void
|
||||
setViewport: (x: number, y: number, width: number, height: number) => void
|
||||
dispose: () => void
|
||||
@@ -46,6 +46,7 @@ interface Renderer {
|
||||
|
||||
export const RendererParams = {
|
||||
backgroundColor: PD.Color(Color(0x000000), { description: 'Background color of the 3D canvas' }),
|
||||
transparentBackground: PD.Boolean(false, { description: 'Background opacity of the 3D canvas' }),
|
||||
pickingAlphaThreshold: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }, { description: 'The minimum opacity value needed for an object to be pickable.' }),
|
||||
interiorDarkening: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }),
|
||||
|
||||
@@ -59,39 +60,40 @@ export const RendererParams = {
|
||||
export type RendererProps = PD.Values<typeof RendererParams>
|
||||
|
||||
namespace Renderer {
|
||||
export function create(ctx: WebGLContext, camera: Camera, props: Partial<RendererProps> = {}): Renderer {
|
||||
export function create(ctx: WebGLContext, props: Partial<RendererProps> = {}): Renderer {
|
||||
const { gl, state, stats } = ctx
|
||||
const p = deepClone({ ...PD.getDefaultValues(RendererParams), ...props })
|
||||
|
||||
const viewport = Viewport()
|
||||
const bgColor = Color.toVec3Normalized(Vec3(), p.backgroundColor)
|
||||
|
||||
const view = Mat4.clone(camera.view)
|
||||
const invView = Mat4.invert(Mat4.identity(), view)
|
||||
const modelView = Mat4.clone(camera.view)
|
||||
const invModelView = Mat4.invert(Mat4.identity(), modelView)
|
||||
const invProjection = Mat4.invert(Mat4.identity(), camera.projection)
|
||||
const modelViewProjection = Mat4.mul(Mat4.identity(), modelView, camera.projection)
|
||||
const invModelViewProjection = Mat4.invert(Mat4.identity(), modelViewProjection)
|
||||
const view = Mat4()
|
||||
const invView = Mat4()
|
||||
const modelView = Mat4()
|
||||
const invModelView = Mat4()
|
||||
const invProjection = Mat4()
|
||||
const modelViewProjection = Mat4()
|
||||
const invModelViewProjection = Mat4()
|
||||
|
||||
const viewOffset = camera.viewOffset.enabled ? Vec2.create(camera.viewOffset.offsetX * 16, camera.viewOffset.offsetY * 16) : Vec2()
|
||||
const viewOffset = Vec2()
|
||||
|
||||
const globalUniforms: GlobalUniformValues = {
|
||||
uModel: ValueCell.create(Mat4.identity()),
|
||||
uView: ValueCell.create(camera.view),
|
||||
uView: ValueCell.create(view),
|
||||
uInvView: ValueCell.create(invView),
|
||||
uModelView: ValueCell.create(modelView),
|
||||
uInvModelView: ValueCell.create(invModelView),
|
||||
uInvProjection: ValueCell.create(invProjection),
|
||||
uProjection: ValueCell.create(Mat4.clone(camera.projection)),
|
||||
uProjection: ValueCell.create(Mat4()),
|
||||
uModelViewProjection: ValueCell.create(modelViewProjection),
|
||||
uInvModelViewProjection: ValueCell.create(invModelViewProjection),
|
||||
|
||||
uIsOrtho: ValueCell.create(camera.state.mode === 'orthographic' ? 1 : 0),
|
||||
uIsOrtho: ValueCell.create(1),
|
||||
uViewOffset: ValueCell.create(viewOffset),
|
||||
|
||||
uPixelRatio: ValueCell.create(ctx.pixelRatio),
|
||||
uViewportHeight: ValueCell.create(viewport.height),
|
||||
uViewport: ValueCell.create(Viewport.toVec4(Vec4(), viewport)),
|
||||
uViewOffset: ValueCell.create(viewOffset),
|
||||
|
||||
uLightIntensity: ValueCell.create(p.lightIntensity),
|
||||
uAmbientIntensity: ValueCell.create(p.ambientIntensity),
|
||||
@@ -100,13 +102,14 @@ namespace Renderer {
|
||||
uRoughness: ValueCell.create(p.roughness),
|
||||
uReflectivity: ValueCell.create(p.reflectivity),
|
||||
|
||||
uCameraPosition: ValueCell.create(Vec3.clone(camera.state.position)),
|
||||
uNear: ValueCell.create(camera.near),
|
||||
uFar: ValueCell.create(camera.far),
|
||||
uFogNear: ValueCell.create(camera.fogNear),
|
||||
uFogFar: ValueCell.create(camera.fogFar),
|
||||
uCameraPosition: ValueCell.create(Vec3()),
|
||||
uNear: ValueCell.create(1),
|
||||
uFar: ValueCell.create(10000),
|
||||
uFogNear: ValueCell.create(1),
|
||||
uFogFar: ValueCell.create(10000),
|
||||
uFogColor: ValueCell.create(bgColor),
|
||||
|
||||
uTransparentBackground: ValueCell.create(p.transparentBackground ? 1 : 0),
|
||||
uPickingAlphaThreshold: ValueCell.create(p.pickingAlphaThreshold),
|
||||
uInteriorDarkening: ValueCell.create(p.interiorDarkening),
|
||||
}
|
||||
@@ -158,7 +161,7 @@ namespace Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
const render = (scene: Scene, variant: GraphicsRenderVariant, clear: boolean) => {
|
||||
const render = (scene: Scene, camera: Camera, variant: GraphicsRenderVariant, clear: boolean) => {
|
||||
ValueCell.update(globalUniforms.uModel, scene.view)
|
||||
ValueCell.update(globalUniforms.uView, camera.view)
|
||||
ValueCell.update(globalUniforms.uInvView, Mat4.invert(invView, camera.view))
|
||||
@@ -191,7 +194,7 @@ namespace Renderer {
|
||||
|
||||
if (clear) {
|
||||
if (variant === 'color') {
|
||||
state.clearColor(bgColor[0], bgColor[1], bgColor[2], 1.0)
|
||||
state.clearColor(bgColor[0], bgColor[1], bgColor[2], p.transparentBackground ? 0 : 1)
|
||||
} else {
|
||||
state.clearColor(1, 1, 1, 1)
|
||||
}
|
||||
@@ -204,7 +207,7 @@ namespace Renderer {
|
||||
if (r.state.opaque) renderObject(r, variant)
|
||||
}
|
||||
|
||||
state.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
state.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE)
|
||||
state.enable(gl.BLEND)
|
||||
for (let i = 0, il = renderables.length; i < il; ++i) {
|
||||
const r = renderables[i]
|
||||
@@ -224,7 +227,7 @@ namespace Renderer {
|
||||
clear: () => {
|
||||
state.depthMask(true)
|
||||
state.colorMask(true, true, true, true)
|
||||
state.clearColor(bgColor[0], bgColor[1], bgColor[2], 1.0)
|
||||
state.clearColor(bgColor[0], bgColor[1], bgColor[2], p.transparentBackground ? 0 : 1)
|
||||
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
|
||||
},
|
||||
render,
|
||||
@@ -243,6 +246,10 @@ namespace Renderer {
|
||||
Color.toVec3Normalized(bgColor, p.backgroundColor)
|
||||
ValueCell.update(globalUniforms.uFogColor, Vec3.copy(globalUniforms.uFogColor.ref.value, bgColor))
|
||||
}
|
||||
if (props.transparentBackground !== undefined && props.transparentBackground !== p.transparentBackground) {
|
||||
p.transparentBackground = props.transparentBackground
|
||||
ValueCell.update(globalUniforms.uTransparentBackground, p.transparentBackground ? 1 : 0)
|
||||
}
|
||||
if (props.lightIntensity !== undefined && props.lightIntensity !== p.lightIntensity) {
|
||||
p.lightIntensity = props.lightIntensity
|
||||
ValueCell.update(globalUniforms.uLightIntensity, p.lightIntensity)
|
||||
|
||||
@@ -2,10 +2,11 @@ export default `
|
||||
#ifdef dUseFog
|
||||
float depth = length(vViewPosition);
|
||||
float fogFactor = smoothstep(uFogNear, uFogFar, depth);
|
||||
gl_FragColor.rgb = mix(gl_FragColor.rgb, uFogColor, fogFactor);
|
||||
float fogAlpha = (1.0 - fogFactor) * gl_FragColor.a;
|
||||
if (fogAlpha < 0.01)
|
||||
discard;
|
||||
gl_FragColor = vec4(gl_FragColor.rgb, fogAlpha);
|
||||
if (uTransparentBackground == 0) {
|
||||
gl_FragColor.rgb = mix(gl_FragColor.rgb, uFogColor, fogFactor);
|
||||
} else {
|
||||
float fogAlpha = (1.0 - fogFactor) * gl_FragColor.a;
|
||||
gl_FragColor.a = fogAlpha;
|
||||
}
|
||||
#endif
|
||||
`
|
||||
@@ -22,6 +22,7 @@ uniform vec3 uFogColor;
|
||||
uniform float uAlpha;
|
||||
uniform float uPickingAlphaThreshold;
|
||||
uniform int uPickable;
|
||||
uniform int uTransparentBackground;
|
||||
|
||||
uniform float uInteriorDarkening;
|
||||
`
|
||||
@@ -190,6 +190,7 @@ export interface WebGLContext {
|
||||
readonly framebufferCache: FramebufferCache
|
||||
|
||||
readonly maxTextureSize: number
|
||||
readonly maxRenderbufferSize: number
|
||||
readonly maxDrawBuffers: number
|
||||
|
||||
unbindFramebuffer: () => void
|
||||
@@ -212,6 +213,7 @@ export function createContext(gl: GLRenderingContext): WebGLContext {
|
||||
|
||||
const parameters = {
|
||||
maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE) as number,
|
||||
maxRenderbufferSize: gl.getParameter(gl.MAX_RENDERBUFFER_SIZE) as number,
|
||||
maxDrawBuffers: isWebGL2(gl) ? gl.getParameter(gl.MAX_DRAW_BUFFERS) as number : 0,
|
||||
maxVertexTextureImageUnits: gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS) as number,
|
||||
}
|
||||
@@ -274,6 +276,7 @@ export function createContext(gl: GLRenderingContext): WebGLContext {
|
||||
framebufferCache,
|
||||
|
||||
get maxTextureSize () { return parameters.maxTextureSize },
|
||||
get maxRenderbufferSize () { return parameters.maxRenderbufferSize },
|
||||
get maxDrawBuffers () { return parameters.maxDrawBuffers },
|
||||
|
||||
unbindFramebuffer: () => unbindFramebuffer(gl),
|
||||
|
||||
@@ -104,7 +104,7 @@ namespace Loci {
|
||||
export function getBoundingSphere(loci: Loci, boundingSphere?: Sphere3D): Sphere3D | undefined {
|
||||
if (loci.kind === 'every-loci' || loci.kind === 'empty-loci') return void 0;
|
||||
|
||||
if (!boundingSphere) boundingSphere = Sphere3D.zero()
|
||||
if (!boundingSphere) boundingSphere = Sphere3D()
|
||||
sphereHelper.reset();
|
||||
|
||||
if (loci.kind === 'structure-loci') {
|
||||
|
||||
@@ -191,7 +191,13 @@ async function findMatesRadius(ctx: RuntimeContext, structure: Structure, radius
|
||||
const operators = getOperatorsCached333(symmetry, modelCenter);
|
||||
const lookup = structure.lookup3d;
|
||||
|
||||
const assembler = Structure.Builder();
|
||||
// keep track of added invariant-unit and operator combinations
|
||||
const added = new Set<string>()
|
||||
function hash(unit: Unit, oper: SymmetryOperator) {
|
||||
return `${unit.invariantId}|${oper.name}`
|
||||
}
|
||||
|
||||
const assembler = Structure.Builder({ label: structure.label });
|
||||
|
||||
const { units } = structure;
|
||||
const center = Vec3.zero();
|
||||
@@ -204,13 +210,17 @@ async function findMatesRadius(ctx: RuntimeContext, structure: Structure, radius
|
||||
for (let uI = 0, _uI = closeUnits.count; uI < _uI; uI++) {
|
||||
const closeUnit = units[closeUnits.indices[uI]];
|
||||
if (!closeUnit.lookup3d.check(center[0], center[1], center[2], boundingSphere.radius + radius)) continue;
|
||||
assembler.addWithOperator(unit, oper);
|
||||
|
||||
const h = hash(unit, oper)
|
||||
if (!added.has(h)) {
|
||||
assembler.addWithOperator(unit, oper);
|
||||
added.add(h)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ctx.shouldUpdate) await ctx.update('Building symmetry...');
|
||||
}
|
||||
|
||||
|
||||
return assembler.getStructure();
|
||||
}
|
||||
|
||||
|
||||
@@ -36,12 +36,11 @@ export const FocusLoci = PluginBehavior.create<FocusLociProps>({
|
||||
if (!this.ctx.canvas3d) return;
|
||||
|
||||
const p = this.params;
|
||||
const durationMs = typeof p.durationMs === 'undefined' ? 250 : p.durationMs;
|
||||
if (Binding.match(this.params.bindings.clickCenterFocus, buttons, modifiers)) {
|
||||
const sphere = Loci.getBoundingSphere(current.loci);
|
||||
if (sphere) {
|
||||
const radius = Math.max(sphere.radius + p.extraRadius, p.minRadius);
|
||||
this.ctx.canvas3d.camera.focus(sphere.center, radius, durationMs);
|
||||
this.ctx.canvas3d.camera.focus(sphere.center, radius, p.durationMs);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -42,6 +42,7 @@ export const DefaultPluginSpec: PluginSpec = {
|
||||
PluginSpec.Action(StateTransforms.Model.TrajectoryFromPDB),
|
||||
PluginSpec.Action(StateTransforms.Model.StructureAssemblyFromModel),
|
||||
PluginSpec.Action(StateTransforms.Model.StructureSymmetryFromModel),
|
||||
PluginSpec.Action(StateTransforms.Model.StructureSymmetryMatesFromModel),
|
||||
PluginSpec.Action(TransformStructureConformation),
|
||||
PluginSpec.Action(StateTransforms.Model.StructureFromModel),
|
||||
PluginSpec.Action(StateTransforms.Model.StructureFromTrajectory),
|
||||
|
||||
@@ -362,4 +362,25 @@
|
||||
margin: 0 ($control-spacing / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.msp-image-preview {
|
||||
position: relative;
|
||||
background: $default-background;
|
||||
margin-top: 1px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
> canvas {
|
||||
max-height: 200px;
|
||||
border-width: 0px 1px 0px 1px;
|
||||
border-style: solid;
|
||||
border-color: $border-color;
|
||||
|
||||
background-color: $default-background;
|
||||
background-image: linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey),
|
||||
linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 10px 10px;
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,14 @@
|
||||
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||
-webkit-touch-callout: none;
|
||||
touch-action: manipulation;
|
||||
|
||||
> canvas {
|
||||
background-color: $default-background;
|
||||
background-image: linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey),
|
||||
linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey);
|
||||
background-size: 60px 60px;
|
||||
background-position: 0 0, 30px 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.msp-viewport-controls {
|
||||
|
||||
@@ -40,6 +40,7 @@ export { StructureFromTrajectory };
|
||||
export { StructureFromModel };
|
||||
export { StructureAssemblyFromModel };
|
||||
export { StructureSymmetryFromModel };
|
||||
export { StructureSymmetryMatesFromModel };
|
||||
export { TransformStructureConformation };
|
||||
export { TransformStructureConformationByMatrix };
|
||||
export { StructureSelectionFromExpression };
|
||||
@@ -301,6 +302,31 @@ const StructureSymmetryFromModel = PluginStateTransform.BuiltIn({
|
||||
}
|
||||
});
|
||||
|
||||
type StructureSymmetryMatesFromModel = typeof StructureSymmetryMatesFromModel
|
||||
const StructureSymmetryMatesFromModel = PluginStateTransform.BuiltIn({
|
||||
name: 'structure-symmetry-mates-from-model',
|
||||
display: { name: 'Structure Symmetry Mates', description: 'Create molecular structure symmetry mates.' },
|
||||
from: SO.Molecule.Model,
|
||||
to: SO.Molecule.Structure,
|
||||
params(a) {
|
||||
return {
|
||||
radius: PD.Numeric(5),
|
||||
}
|
||||
}
|
||||
})({
|
||||
apply({ a, params }, plugin: PluginContext) {
|
||||
return Task.create('Build Symmetry Mates', async ctx => {
|
||||
const { radius } = params
|
||||
const model = a.data;
|
||||
const base = Structure.ofModel(model);
|
||||
const s = await StructureSymmetry.builderSymmetryMates(base, radius).runInContext(ctx);
|
||||
await ensureSecondaryStructure(s)
|
||||
const props = { label: `Symmetry Mates`, description: structureDesc(s) };
|
||||
return new SO.Molecule.Structure(s, props);
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
const _translation = Vec3.zero(), _m = Mat4.zero(), _n = Mat4.zero();
|
||||
type TransformStructureConformation = typeof TransformStructureConformation
|
||||
const TransformStructureConformation = PluginStateTransform.BuiltIn({
|
||||
|
||||
@@ -95,6 +95,10 @@ export abstract class CollapsableControls<P extends CollapsableProps = Collapsab
|
||||
|
||||
constructor(props: P, context?: any) {
|
||||
super(props, context)
|
||||
this.state = this.defaultState()
|
||||
|
||||
const state = this.defaultState()
|
||||
if (props.initiallyCollapsed !== undefined) state.isCollapsed = props.initiallyCollapsed
|
||||
if (props.header !== undefined) state.header = props.header
|
||||
this.state = state
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { camelCaseToWords } from '../../../mol-util/string';
|
||||
import * as React from 'react';
|
||||
import { _Props, _State } from '../base';
|
||||
import { ParamProps } from './parameters';
|
||||
import { TextInput } from './common';
|
||||
|
||||
export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Color>, { isExpanded: boolean }> {
|
||||
state = { isExpanded: false }
|
||||
@@ -38,6 +39,12 @@ export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Colo
|
||||
}
|
||||
}
|
||||
|
||||
onChangeText = (value: Color) => {
|
||||
if (value !== this.props.value) {
|
||||
this.update(value);
|
||||
}
|
||||
}
|
||||
|
||||
swatch() {
|
||||
// const def = this.props.param.defaultValue;
|
||||
return <div className='msp-combined-color-swatch'>
|
||||
@@ -46,25 +53,6 @@ export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Colo
|
||||
</div>;
|
||||
}
|
||||
|
||||
// TODO: include text options as well?
|
||||
// onChangeText = () => {};
|
||||
// text() {
|
||||
// const [r, g, b] = Color.toRgb(this.props.value);
|
||||
// return <input type='text'
|
||||
// value={`${r} ${g} ${b}`}
|
||||
// placeholder={'Red Green Blue'}
|
||||
// onChange={this.onChangeText}
|
||||
// onKeyPress={this.props.onEnter ? this.onKeyPress : void 0}
|
||||
// disabled={this.props.isDisabled}
|
||||
// />;
|
||||
// }
|
||||
// onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
// if (!this.props.onEnter) return;
|
||||
// if ((e.keyCode === 13 || e.charCode === 13)) {
|
||||
// this.props.onEnter();
|
||||
// }
|
||||
// }
|
||||
|
||||
stripStyle(): React.CSSProperties {
|
||||
return {
|
||||
background: Color.toStyle(this.props.value),
|
||||
@@ -76,7 +64,6 @@ export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Colo
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const label = this.props.param.label || camelCaseToWords(this.props.name);
|
||||
return <>
|
||||
@@ -89,7 +76,17 @@ export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Colo
|
||||
{this.state.isExpanded && <div className='msp-control-offset'>
|
||||
{this.swatch()}
|
||||
<div className='msp-control-row'>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<span>RGB</span>
|
||||
<div>
|
||||
<TextInput onChange={this.onChangeText} value={this.props.value}
|
||||
fromValue={formatColorRGB} toValue={getColorFromString} isValid={isValidColorString}
|
||||
className='msp-form-control' onEnter={this.props.onEnter} blurOnEnter={true} blurOnEscape={true}
|
||||
placeholder='e.g. 127 127 127' delayMs={250} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='msp-control-row'>
|
||||
<span>Color List</span>
|
||||
<div>
|
||||
<select value={this.props.value} onChange={this.onChangeSelect}>
|
||||
{ColorValueOption(this.props.value)}
|
||||
{ColorOptions()}
|
||||
@@ -102,6 +99,28 @@ export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Colo
|
||||
}
|
||||
}
|
||||
|
||||
function formatColorRGB(c: Color) {
|
||||
const [r, g, b] = Color.toRgb(c);
|
||||
return `${r} ${g} ${b}`;
|
||||
}
|
||||
|
||||
function getColorFromString(s: string) {
|
||||
const cs = s.split(/\s+/g);
|
||||
return Color.fromRgb(+cs[0], +cs[1], +cs[2]);
|
||||
}
|
||||
|
||||
function isValidColorString(s: string) {
|
||||
const cs = s.split(/\s+/g);
|
||||
if (cs.length !== 3 && !(cs.length === 4 && cs[3] === '')) return false;
|
||||
for (const c of cs) {
|
||||
if (c === '') continue;
|
||||
const n = +c;
|
||||
if ('' + n !== c) return false;
|
||||
if (n < 0 || n > 255) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// the 1st color is the default value.
|
||||
const SwatchColors = [
|
||||
0x000000, 0x808080, 0xFFFFFF, 0xD33115, 0xE27300, 0xFCC400,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { PurePluginUIComponent } from '../base';
|
||||
|
||||
export class ControlGroup extends React.Component<{ header: string, initialExpanded?: boolean }, { isExpanded: boolean }> {
|
||||
state = { isExpanded: !!this.props.initialExpanded }
|
||||
@@ -28,6 +29,128 @@ export class ControlGroup extends React.Component<{ header: string, initialExpan
|
||||
}
|
||||
}
|
||||
|
||||
export interface TextInputProps<T> {
|
||||
className?: string,
|
||||
style?: React.CSSProperties,
|
||||
value: T,
|
||||
fromValue?(v: T): string,
|
||||
toValue?(s: string): T,
|
||||
// TODO: add error/help messages here?
|
||||
isValid?(s: string): boolean,
|
||||
onChange(value: T): void,
|
||||
onEnter?(): void,
|
||||
onBlur?(): void,
|
||||
delayMs?: number,
|
||||
blurOnEnter?: boolean,
|
||||
blurOnEscape?: boolean,
|
||||
isDisabled?: boolean,
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
interface TextInputState {
|
||||
originalValue: string,
|
||||
value: string
|
||||
}
|
||||
|
||||
function _id(x: any) { return x; }
|
||||
|
||||
export class TextInput<T = string> extends PurePluginUIComponent<TextInputProps<T>, TextInputState> {
|
||||
private input = React.createRef<HTMLInputElement>();
|
||||
private delayHandle: any = void 0;
|
||||
private pendingValue: T | undefined = void 0;
|
||||
|
||||
state = { originalValue: '', value: '' }
|
||||
|
||||
onBlur = () => {
|
||||
this.setState({ value: '' + this.state.originalValue });
|
||||
if (this.props.onBlur) this.props.onBlur();
|
||||
}
|
||||
|
||||
get isPending() { return typeof this.delayHandle !== 'undefined'; }
|
||||
|
||||
clearTimeout() {
|
||||
if (this.isPending) {
|
||||
clearTimeout(this.delayHandle);
|
||||
this.delayHandle = void 0;
|
||||
}
|
||||
}
|
||||
|
||||
raiseOnChange = () => {
|
||||
this.props.onChange(this.pendingValue!);
|
||||
this.pendingValue = void 0;
|
||||
}
|
||||
|
||||
triggerChanged(formatted: string, converted: T) {
|
||||
this.clearTimeout();
|
||||
|
||||
if (formatted === this.state.originalValue) return;
|
||||
|
||||
if (this.props.delayMs) {
|
||||
this.pendingValue = converted;
|
||||
this.delayHandle = setTimeout(this.raiseOnChange, this.props.delayMs);
|
||||
} else {
|
||||
this.props.onChange(converted);
|
||||
}
|
||||
}
|
||||
|
||||
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
|
||||
if (this.props.isValid && !this.props.isValid(value)) {
|
||||
this.clearTimeout();
|
||||
this.setState({ value });
|
||||
return;
|
||||
}
|
||||
|
||||
const converted = (this.props.toValue || _id)(value);
|
||||
const formatted = (this.props.fromValue || _id)(converted);
|
||||
this.setState({ value: formatted }, () => this.triggerChanged(formatted, converted));
|
||||
}
|
||||
|
||||
onKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.charCode === 27 || e.keyCode === 27 /* esc */) {
|
||||
if (this.props.blurOnEscape && this.input.current) {
|
||||
this.input.current.blur();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.keyCode === 13 || e.charCode === 13 /* enter */) {
|
||||
if (this.isPending) {
|
||||
this.clearTimeout();
|
||||
this.raiseOnChange();
|
||||
}
|
||||
if (this.props.blurOnEnter && this.input.current) {
|
||||
this.input.current.blur();
|
||||
}
|
||||
if (this.props.onEnter) this.props.onEnter();
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: TextInputProps<any>, state: TextInputState) {
|
||||
const value = props.fromValue ? props.fromValue(props.value) : props.value;
|
||||
if (value === state.originalValue) return null;
|
||||
return { originalValue: value, value };
|
||||
}
|
||||
|
||||
render() {
|
||||
return <input type='text'
|
||||
className={this.props.className}
|
||||
style={this.props.style}
|
||||
ref={this.input}
|
||||
onBlur={this.onBlur}
|
||||
value={this.state.value}
|
||||
placeholder={this.props.placeholder}
|
||||
onChange={this.onChange}
|
||||
onKeyPress={this.props.onEnter || this.props.blurOnEnter || this.props.blurOnEscape ? this.onKeyPress : void 0}
|
||||
onKeyDown={this.props.blurOnEscape ? this.onKeyUp : void 0}
|
||||
disabled={!!this.props.isDisabled}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: replace this with parametrized TextInput
|
||||
export class NumericInput extends React.PureComponent<{
|
||||
value: number,
|
||||
onChange: (v: number) => void,
|
||||
|
||||
204
src/mol-plugin/ui/image.tsx
Normal file
204
src/mol-plugin/ui/image.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { CollapsableControls, CollapsableState } from './base';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { ParameterControls } from './controls/parameters';
|
||||
import { ImagePass } from '../../mol-canvas3d/passes/image';
|
||||
import { download } from '../../mol-util/download';
|
||||
import { setCanvasSize, canvasToBlob } from '../../mol-canvas3d/util';
|
||||
import { Task } from '../../mol-task';
|
||||
|
||||
interface ImageControlsState extends CollapsableState {
|
||||
showPreview: boolean
|
||||
|
||||
size: 'canvas' | 'custom'
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
const maxWidthUi = 260
|
||||
const maxHeightUi = 180
|
||||
|
||||
export class ImageControls<P, S extends ImageControlsState> extends CollapsableControls<P, S> {
|
||||
private canvasRef = React.createRef<HTMLCanvasElement>()
|
||||
|
||||
private canvas: HTMLCanvasElement
|
||||
private canvasContext: CanvasRenderingContext2D
|
||||
|
||||
private imagePass: ImagePass
|
||||
|
||||
constructor(props: P, context?: any) {
|
||||
super(props, context)
|
||||
|
||||
this.subscribe(this.plugin.events.canvas3d.initialized, () => this.forceUpdate())
|
||||
}
|
||||
|
||||
private getSize() {
|
||||
return this.state.size === 'canvas' ? {
|
||||
width: this.plugin.canvas3d.webgl.gl.drawingBufferWidth,
|
||||
height: this.plugin.canvas3d.webgl.gl.drawingBufferHeight
|
||||
} : {
|
||||
width: this.state.width,
|
||||
height: this.state.height
|
||||
}
|
||||
}
|
||||
|
||||
private preview = () => {
|
||||
const { width, height } = this.getSize()
|
||||
if (width <= 0 || height <= 0) return
|
||||
|
||||
let w: number, h: number
|
||||
const aH = maxHeightUi / height
|
||||
const aW = maxWidthUi / width
|
||||
if (aH < aW) {
|
||||
h = Math.round(Math.min(maxHeightUi, height))
|
||||
w = Math.round(width * (h / height))
|
||||
} else {
|
||||
w = Math.round(Math.min(maxWidthUi, width))
|
||||
h = Math.round(height * (w / width))
|
||||
}
|
||||
setCanvasSize(this.canvas, w, h)
|
||||
const { pixelRatio } = this.plugin.canvas3d.webgl
|
||||
const imageData = this.imagePass.getImageData(w * pixelRatio, h * pixelRatio)
|
||||
this.canvasContext.putImageData(imageData, 0, 0)
|
||||
}
|
||||
|
||||
private downloadTask = () => {
|
||||
return Task.create('Download Image', async ctx => {
|
||||
const { width, height } = this.getSize()
|
||||
if (width <= 0 || height <= 0) return
|
||||
|
||||
await ctx.update('Rendering image...')
|
||||
const imageData = this.imagePass.getImageData(width, height)
|
||||
|
||||
await ctx.update('Encoding image...')
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = imageData.width
|
||||
canvas.height = imageData.height
|
||||
const canvasCtx = canvas.getContext('2d')
|
||||
if (!canvasCtx) throw new Error('Could not create canvas 2d context')
|
||||
canvasCtx.putImageData(imageData, 0, 0)
|
||||
|
||||
await ctx.update('Downloading image...')
|
||||
const blob = await canvasToBlob(canvas)
|
||||
download(blob, 'molstar-image')
|
||||
})
|
||||
}
|
||||
|
||||
private download = () => {
|
||||
this.plugin.runTask(this.downloadTask())
|
||||
}
|
||||
|
||||
private syncCanvas() {
|
||||
if (!this.canvasRef.current) return
|
||||
if (this.canvasRef.current === this.canvas) return
|
||||
|
||||
this.canvas = this.canvasRef.current
|
||||
const ctx = this.canvas.getContext('2d')
|
||||
if (!ctx) throw new Error('Could not get canvas 2d context')
|
||||
this.canvasContext = ctx
|
||||
}
|
||||
|
||||
private handlePreview() {
|
||||
if (this.state.showPreview) {
|
||||
this.syncCanvas()
|
||||
this.preview()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.handlePreview()
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.imagePass = this.plugin.canvas3d.getImagePass()
|
||||
this.imagePass.setProps({
|
||||
multiSample: { mode: 'on', sampleLevel: 2 },
|
||||
postprocessing: this.plugin.canvas3d.props.postprocessing
|
||||
})
|
||||
|
||||
this.handlePreview()
|
||||
|
||||
this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => {
|
||||
this.imagePass.setProps({
|
||||
multiSample: { mode: 'on', sampleLevel: 2 },
|
||||
postprocessing: this.plugin.canvas3d.props.postprocessing
|
||||
})
|
||||
this.handlePreview()
|
||||
})
|
||||
|
||||
this.subscribe(this.plugin.canvas3d.didDraw, () => this.handlePreview())
|
||||
}
|
||||
|
||||
private togglePreview = () => this.setState({ showPreview: !this.state.showPreview })
|
||||
|
||||
private setProps = (p: { param: PD.Base<any>, name: string, value: any }) => {
|
||||
if (p.name === 'size') {
|
||||
if (p.value.name === 'custom') {
|
||||
this.setState({ size: p.value.name, width: p.value.params.width, height: p.value.params.height })
|
||||
} else {
|
||||
this.setState({ size: p.value.name })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private get params () {
|
||||
const max = Math.min(this.plugin.canvas3d ? this.plugin.canvas3d.webgl.maxRenderbufferSize : 4096, 8192)
|
||||
const { width, height } = this.defaultState()
|
||||
return {
|
||||
size: PD.MappedStatic('custom', {
|
||||
canvas: PD.Group({}),
|
||||
custom: PD.Group({
|
||||
width: PD.Numeric(width, { min: 1, max, step: 1 }),
|
||||
height: PD.Numeric(height, { min: 1, max, step: 1 }),
|
||||
}, { isFlat: true })
|
||||
}, { options: [['canvas', 'Canvas'], ['custom', 'Custom']] })
|
||||
}
|
||||
}
|
||||
|
||||
private get values () {
|
||||
return this.state.size === 'canvas'
|
||||
? { size: { name: 'canvas', params: {} } }
|
||||
: { size: { name: 'custom', params: { width: this.state.width, height: this.state.height } } }
|
||||
}
|
||||
|
||||
protected defaultState() {
|
||||
return {
|
||||
isCollapsed: false,
|
||||
header: 'Create Image',
|
||||
|
||||
showPreview: false,
|
||||
|
||||
size: 'canvas',
|
||||
width: 1920,
|
||||
height: 1080
|
||||
} as S
|
||||
}
|
||||
|
||||
protected renderControls() {
|
||||
return <div>
|
||||
<div className='msp-control-row'>
|
||||
<button className='msp-btn msp-btn-block' onClick={this.download}>Download</button>
|
||||
</div>
|
||||
<ParameterControls params={this.params} values={this.values} onChange={this.setProps} />
|
||||
<div className='msp-control-group-wrapper'>
|
||||
<div className='msp-control-group-header'>
|
||||
<button className='msp-btn msp-btn-block' onClick={this.togglePreview}>
|
||||
<span className={`msp-icon msp-icon-${this.state.showPreview ? 'collapse' : 'expand'}`} />
|
||||
Preview
|
||||
</button>
|
||||
</div>
|
||||
{this.state.showPreview && <div className='msp-control-offset'>
|
||||
<div className='msp-image-preview'>
|
||||
<canvas width='0px' height='0px' ref={this.canvasRef} />
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import { StateTransform } from '../../mol-state';
|
||||
import { UpdateTransformControl } from './state/update-transform';
|
||||
import { SequenceView } from './sequence';
|
||||
import { Toasts } from './toast';
|
||||
import { ImageControls } from './image';
|
||||
|
||||
export class Plugin extends React.Component<{ plugin: PluginContext }, {}> {
|
||||
region(kind: 'left' | 'right' | 'bottom' | 'main', element: JSX.Element) {
|
||||
@@ -129,6 +130,7 @@ export class ControlsWrapper extends PluginUIComponent {
|
||||
{/* <AnimationControlsWrapper /> */}
|
||||
{/* <CameraSnapshots /> */}
|
||||
<StructureToolsWrapper />
|
||||
<ImageControls />
|
||||
<StateSnapshots />
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,8 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
|
||||
this.subscribe(debounceTime<{ seqIdx: number, buttons: number, modifiers: ModifiersKeys }>(15)(this.highlightQueue), (e) => {
|
||||
this.hover(e.seqIdx < 0 ? void 0 : e.seqIdx, e.buttons, e.modifiers);
|
||||
});
|
||||
|
||||
// this.updateMarker()
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -98,13 +100,13 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
|
||||
private updateMarker() {
|
||||
if (!this.parentDiv.current) return;
|
||||
const xs = this.parentDiv.current.children;
|
||||
const markerData = this.props.sequenceWrapper.markerArray;
|
||||
const { markerArray } = this.props.sequenceWrapper;
|
||||
|
||||
for (let i = 0, _i = markerData.length; i < _i; i++) {
|
||||
for (let i = 0, _i = markerArray.length; i < _i; i++) {
|
||||
const span = xs[i] as HTMLSpanElement;
|
||||
if (!span) continue;
|
||||
|
||||
const backgroundColor = this.getBackgroundColor(markerData[i]);
|
||||
const backgroundColor = this.getBackgroundColor(markerArray[i]);
|
||||
if (span.style.backgroundColor !== backgroundColor) span.style.backgroundColor = backgroundColor;
|
||||
}
|
||||
}
|
||||
@@ -142,15 +144,18 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const markerData = this.props.sequenceWrapper.markerArray;
|
||||
const sw = this.props.sequenceWrapper
|
||||
|
||||
const elems: JSX.Element[] = [];
|
||||
for (let i = 0, il = sw.length; i < il; ++i) {
|
||||
elems[elems.length] = this.residue(i, sw.residueLabel(i), markerData[i], sw.residueColor(i));
|
||||
elems[elems.length] = this.residue(i, sw.residueLabel(i), sw.markerArray[i], sw.residueColor(i));
|
||||
// TODO: add seq idx markers every N residues? Would need to modify "updateMarker"
|
||||
}
|
||||
|
||||
// calling .updateMarker here is neccesary to ensure existing
|
||||
// residue spans are updated as react won't update them
|
||||
this.updateMarker()
|
||||
|
||||
return <div
|
||||
className='msp-sequence-wrapper msp-sequence-wrapper-non-empty'
|
||||
onContextMenu={this.contextMenu}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { CollapsableControls } from '../base';
|
||||
import { CollapsableControls, CollapsableState } from '../base';
|
||||
import { StructureSelectionQueries, SelectionModifier } from '../../util/structure-selection-helper';
|
||||
import { ButtonSelect, Options } from '../controls/common';
|
||||
import { PluginCommands } from '../../command';
|
||||
@@ -18,7 +18,13 @@ const StructureSelectionParams = {
|
||||
granularity: Interactivity.Params.granularity,
|
||||
}
|
||||
|
||||
export class StructureSelectionControls extends CollapsableControls {
|
||||
interface StructureSelectionControlsState extends CollapsableState {
|
||||
minRadius: number,
|
||||
extraRadius: number,
|
||||
durationMs: number
|
||||
}
|
||||
|
||||
export class StructureSelectionControls<P, S extends StructureSelectionControlsState> extends CollapsableControls<P, S> {
|
||||
componentDidMount() {
|
||||
this.subscribe(this.plugin.events.interactivity.selectionUpdated, () => {
|
||||
this.forceUpdate()
|
||||
@@ -38,6 +44,14 @@ export class StructureSelectionControls extends CollapsableControls {
|
||||
}
|
||||
}
|
||||
|
||||
focus = () => {
|
||||
const { extraRadius, minRadius, durationMs } = this.state
|
||||
const { sphere } = this.plugin.helpers.structureSelectionManager.getBoundary();
|
||||
if (sphere.radius === 0) return
|
||||
const radius = Math.max(sphere.radius + extraRadius, minRadius);
|
||||
this.plugin.canvas3d.camera.focus(sphere.center, radius, durationMs);
|
||||
}
|
||||
|
||||
setProps = (p: { param: PD.Base<any>, name: string, value: any }) => {
|
||||
if (p.name === 'granularity') {
|
||||
PluginCommands.Interactivity.SetProps.dispatch(this.plugin, { props: { granularity: p.value } });
|
||||
@@ -46,7 +60,7 @@ export class StructureSelectionControls extends CollapsableControls {
|
||||
|
||||
get values () {
|
||||
return {
|
||||
granularity: this.plugin.interactivity.props.granularity
|
||||
granularity: this.plugin.interactivity.props.granularity,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,8 +76,12 @@ export class StructureSelectionControls extends CollapsableControls {
|
||||
defaultState() {
|
||||
return {
|
||||
isCollapsed: false,
|
||||
header: 'Selection'
|
||||
}
|
||||
header: 'Selection',
|
||||
|
||||
minRadius: 8,
|
||||
extraRadius: 4,
|
||||
durationMs: 250
|
||||
} as S
|
||||
}
|
||||
|
||||
renderControls() {
|
||||
@@ -73,7 +91,10 @@ export class StructureSelectionControls extends CollapsableControls {
|
||||
|
||||
return <div>
|
||||
<div className='msp-control-row msp-row-text'>
|
||||
<div>{this.stats}</div>
|
||||
<button className='msp-btn msp-btn-block' onClick={this.focus}>
|
||||
<span className={`msp-icon msp-icon-focus-on-visual`} style={{ position: 'absolute', left: '10px' }} />
|
||||
{this.stats}
|
||||
</button>
|
||||
</div>
|
||||
<ParameterControls params={StructureSelectionParams} values={this.values} onChange={this.setProps} />
|
||||
<div className='msp-control-row'>
|
||||
|
||||
@@ -48,7 +48,7 @@ function getUnitcellMesh(data: UnitcellData, props: UnitcellProps, mesh?: Mesh)
|
||||
Mat4.fromTranslation(tmpTranslate, tmpRef)
|
||||
const cellCage = transformCage(copyCage(unitCage), tmpTranslate)
|
||||
|
||||
const radius = (Math.cbrt(data.symmetry.spacegroup.cell.volume) / 100) * props.cellScale
|
||||
const radius = (Math.cbrt(data.symmetry.spacegroup.cell.volume) / 300) * props.cellScale
|
||||
state.currentGroup = 1
|
||||
MeshBuilder.addCage(state, fromFractional, cellCage, radius, 2, 20)
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@ import { StateObject } from '../../mol-state';
|
||||
import { PluginContext } from '../context';
|
||||
import { PluginStateObject } from '../state/objects';
|
||||
import { structureElementStatsLabel } from '../../mol-theme/label';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
|
||||
import { Boundary } from '../../mol-model/structure/structure/util/boundary';
|
||||
|
||||
const boundaryHelper = new BoundaryHelper();
|
||||
|
||||
export { StructureElementSelectionManager };
|
||||
class StructureElementSelectionManager {
|
||||
@@ -30,6 +35,37 @@ class StructureElementSelectionManager {
|
||||
return this.entries.get(ref)!;
|
||||
}
|
||||
|
||||
getBoundary() {
|
||||
const min = Vec3.create(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE)
|
||||
const max = Vec3.create(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE)
|
||||
|
||||
boundaryHelper.reset(0);
|
||||
|
||||
const boundaries: Boundary[] = []
|
||||
this.entries.forEach(v => {
|
||||
const loci = v.selection
|
||||
if (!StructureElement.Loci.isEmpty(loci)) {
|
||||
boundaries.push(StructureElement.Loci.getBoundary(loci))
|
||||
}
|
||||
})
|
||||
|
||||
for (let i = 0, il = boundaries.length; i < il; ++i) {
|
||||
const { box, sphere } = boundaries[i];
|
||||
Vec3.min(min, min, box.min);
|
||||
Vec3.max(max, max, box.max);
|
||||
boundaryHelper.boundaryStep(sphere.center, sphere.radius)
|
||||
}
|
||||
|
||||
boundaryHelper.finishBoundaryStep();
|
||||
|
||||
for (let i = 0, il = boundaries.length; i < il; ++i) {
|
||||
const { sphere } = boundaries[i];
|
||||
boundaryHelper.extendStep(sphere.center, sphere.radius);
|
||||
}
|
||||
|
||||
return { box: { min, max }, sphere: boundaryHelper.getSphere() };
|
||||
}
|
||||
|
||||
get stats() {
|
||||
let structureCount = 0
|
||||
let elementCount = 0
|
||||
|
||||
@@ -31,6 +31,8 @@ import { IllustrativeColorThemeProvider } from './color/illustrative';
|
||||
import { HydrophobicityColorThemeProvider } from './color/hydrophobicity';
|
||||
import { ModelIndexColorThemeProvider } from './color/model-index';
|
||||
import { OccupancyColorThemeProvider } from './color/occupancy';
|
||||
import { OperatorNameColorThemeProvider } from './color/operator-name';
|
||||
import { OperatorHklColorThemeProvider } from './color/operator-hkl';
|
||||
|
||||
export type LocationColor = (location: Location, isSecondary: boolean) => Color
|
||||
|
||||
@@ -83,6 +85,8 @@ export const BuiltInColorThemes = {
|
||||
'model-index': ModelIndexColorThemeProvider,
|
||||
'molecule-type': MoleculeTypeColorThemeProvider,
|
||||
'occupancy': OccupancyColorThemeProvider,
|
||||
'operator-hkl': OperatorHklColorThemeProvider,
|
||||
'operator-name': OperatorNameColorThemeProvider,
|
||||
'polymer-id': PolymerIdColorThemeProvider,
|
||||
'polymer-index': PolymerIndexColorThemeProvider,
|
||||
'residue-name': ResidueNameColorThemeProvider,
|
||||
|
||||
@@ -26,7 +26,10 @@ export function getChainIdColorThemeParams(ctx: ThemeDataContext) {
|
||||
if (ctx.structure) {
|
||||
if (getAsymIdSerialMap(ctx.structure.root).size > 12) {
|
||||
params.palette.defaultValue.name = 'scale'
|
||||
params.palette.defaultValue.params = { list: 'red-yellow-blue' }
|
||||
params.palette.defaultValue.params = {
|
||||
...params.palette.defaultValue.params,
|
||||
list: 'red-yellow-blue'
|
||||
}
|
||||
}
|
||||
}
|
||||
return params
|
||||
@@ -79,6 +82,9 @@ export function ChainIdColorTheme(ctx: ThemeDataContext, props: PD.Values<ChainI
|
||||
const l = StructureElement.Location.create()
|
||||
const asymIdSerialMap = getAsymIdSerialMap(ctx.structure.root)
|
||||
|
||||
const labelTable = Array.from(asymIdSerialMap.keys())
|
||||
props.palette.params.valueLabel = (i: number) => labelTable[i]
|
||||
|
||||
const palette = getPalette(asymIdSerialMap.size, props)
|
||||
legend = palette.legend
|
||||
|
||||
|
||||
@@ -27,7 +27,10 @@ export function getEntitySourceColorThemeParams(ctx: ThemeDataContext) {
|
||||
if (ctx.structure) {
|
||||
if (getMaps(ctx.structure.root.models).srcKeySerialMap.size > 12) {
|
||||
params.palette.defaultValue.name = 'scale'
|
||||
params.palette.defaultValue.params = { list: 'red-yellow-blue' }
|
||||
params.palette.defaultValue.params = {
|
||||
...params.palette.defaultValue.params,
|
||||
list: 'red-yellow-blue'
|
||||
}
|
||||
}
|
||||
}
|
||||
return params
|
||||
@@ -108,6 +111,13 @@ export function EntitySourceColorTheme(ctx: ThemeDataContext, props: PD.Values<E
|
||||
const { models } = ctx.structure.root
|
||||
const { seqToSrcByModelEntity, srcKeySerialMap } = getMaps(models)
|
||||
|
||||
const labelTable = Array.from(srcKeySerialMap.keys()).map(v => {
|
||||
const l = v.split('|')[2]
|
||||
return l === '1' ? 'Unnamed' : l
|
||||
})
|
||||
labelTable.push('Unknown')
|
||||
props.palette.params.valueLabel = (i: number) => labelTable[i]
|
||||
|
||||
const palette = getPalette(srcKeySerialMap.size + 1, props)
|
||||
legend = palette.legend
|
||||
|
||||
|
||||
124
src/mol-theme/color/operator-hkl.ts
Normal file
124
src/mol-theme/color/operator-hkl.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { StructureElement, Link, Structure } from '../../mol-model/structure';
|
||||
import { Location } from '../../mol-model/location';
|
||||
import { ColorTheme, LocationColor } from '../color';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition'
|
||||
import { ThemeDataContext } from '../theme';
|
||||
import { getPaletteParams, getPalette } from '../../mol-util/color/palette';
|
||||
import { ScaleLegend, TableLegend } from '../../mol-util/legend';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { integerDigitCount } from '../../mol-util/number';
|
||||
|
||||
const DefaultColor = Color(0xCCCCCC)
|
||||
const Description = `Assigns a color based on the operator HKL value of a transformed chain.`
|
||||
|
||||
export const OperatorHklColorThemeParams = {
|
||||
...getPaletteParams({ type: 'set', setList: 'set-3' }),
|
||||
}
|
||||
export type OperatorHklColorThemeParams = typeof OperatorHklColorThemeParams
|
||||
export function getOperatorHklColorThemeParams(ctx: ThemeDataContext) {
|
||||
const params = PD.clone(OperatorHklColorThemeParams)
|
||||
if (ctx.structure) {
|
||||
if (getOperatorHklSerialMap(ctx.structure.root).map.size > 12) {
|
||||
params.palette.defaultValue.name = 'scale'
|
||||
params.palette.defaultValue.params = {
|
||||
...params.palette.defaultValue.params,
|
||||
list: 'red-yellow-blue'
|
||||
}
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
const hklOffset = 10000
|
||||
|
||||
function hklKey(hkl: Vec3) {
|
||||
return hkl.map(v => `${v + hklOffset}`.padStart(5, '0')).join('')
|
||||
}
|
||||
|
||||
function hklKeySplit(key: string) {
|
||||
const len = integerDigitCount(hklOffset, 0)
|
||||
const h = parseInt(key.substr(0, len))
|
||||
const k = parseInt(key.substr(len, len))
|
||||
const l = parseInt(key.substr(len + len, len))
|
||||
return [ h - hklOffset, k - hklOffset, l - hklOffset ] as Vec3
|
||||
}
|
||||
|
||||
function formatHkl(hkl: Vec3) {
|
||||
return hkl.map(v => v + 5).join('')
|
||||
}
|
||||
|
||||
function getOperatorHklSerialMap(structure: Structure) {
|
||||
const map = new Map<string, number>()
|
||||
const set = new Set<string>()
|
||||
for (let i = 0, il = structure.units.length; i < il; ++i) {
|
||||
const k = hklKey(structure.units[i].conformation.operator.hkl)
|
||||
set.add(k)
|
||||
}
|
||||
const arr = Array.from(set).sort()
|
||||
arr.forEach(k => map.set(k, map.size))
|
||||
const min = hklKeySplit(arr[0])
|
||||
const max = hklKeySplit(arr[arr.length - 1])
|
||||
return { min, max, map }
|
||||
}
|
||||
|
||||
export function OperatorHklColorTheme(ctx: ThemeDataContext, props: PD.Values<OperatorHklColorThemeParams>): ColorTheme<OperatorHklColorThemeParams> {
|
||||
let color: LocationColor
|
||||
let legend: ScaleLegend | TableLegend | undefined
|
||||
|
||||
if (ctx.structure) {
|
||||
const { min, max, map } = getOperatorHklSerialMap(ctx.structure.root)
|
||||
|
||||
const labelTable: string[] = []
|
||||
map.forEach((v, k) => {
|
||||
const i = v % map.size
|
||||
const label = formatHkl(hklKeySplit(k))
|
||||
if (labelTable[i] === undefined) labelTable[i] = label
|
||||
else labelTable[i] += `, ${label}`
|
||||
})
|
||||
|
||||
props.palette.params.minLabel = formatHkl(min)
|
||||
props.palette.params.maxLabel = formatHkl(max)
|
||||
props.palette.params.valueLabel = (i: number) => labelTable[i]
|
||||
|
||||
const palette = getPalette(map.size, props)
|
||||
legend = palette.legend
|
||||
|
||||
color = (location: Location): Color => {
|
||||
let serial: number | undefined = undefined
|
||||
if (StructureElement.Location.is(location)) {
|
||||
const k = hklKey(location.unit.conformation.operator.hkl)
|
||||
serial = map.get(k)
|
||||
} else if (Link.isLocation(location)) {
|
||||
const k = hklKey(location.aUnit.conformation.operator.hkl)
|
||||
serial = map.get(k)
|
||||
}
|
||||
return serial === undefined ? DefaultColor : palette.color(serial)
|
||||
}
|
||||
} else {
|
||||
color = () => DefaultColor
|
||||
}
|
||||
|
||||
return {
|
||||
factory: OperatorHklColorTheme,
|
||||
granularity: 'instance',
|
||||
color,
|
||||
props,
|
||||
description: Description,
|
||||
legend
|
||||
}
|
||||
}
|
||||
|
||||
export const OperatorHklColorThemeProvider: ColorTheme.Provider<OperatorHklColorThemeParams> = {
|
||||
label: 'Operator HKL',
|
||||
factory: OperatorHklColorTheme,
|
||||
getParams: getOperatorHklColorThemeParams,
|
||||
defaultValues: PD.getDefaultValues(OperatorHklColorThemeParams),
|
||||
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure
|
||||
}
|
||||
90
src/mol-theme/color/operator-name.ts
Normal file
90
src/mol-theme/color/operator-name.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { StructureElement, Link, Structure } from '../../mol-model/structure';
|
||||
import { Location } from '../../mol-model/location';
|
||||
import { ColorTheme, LocationColor } from '../color';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition'
|
||||
import { ThemeDataContext } from '../theme';
|
||||
import { getPaletteParams, getPalette } from '../../mol-util/color/palette';
|
||||
import { ScaleLegend, TableLegend } from '../../mol-util/legend';
|
||||
|
||||
const DefaultColor = Color(0xCCCCCC)
|
||||
const Description = `Assigns a color based on the operator name of a transformed chain.`
|
||||
|
||||
export const OperatorNameColorThemeParams = {
|
||||
...getPaletteParams({ type: 'set', setList: 'set-3' }),
|
||||
}
|
||||
export type OperatorNameColorThemeParams = typeof OperatorNameColorThemeParams
|
||||
export function getOperatorNameColorThemeParams(ctx: ThemeDataContext) {
|
||||
const params = PD.clone(OperatorNameColorThemeParams)
|
||||
if (ctx.structure) {
|
||||
if (getOperatorNameSerialMap(ctx.structure.root).size > 12) {
|
||||
params.palette.defaultValue.name = 'scale'
|
||||
params.palette.defaultValue.params = {
|
||||
...params.palette.defaultValue.params,
|
||||
list: 'red-yellow-blue'
|
||||
}
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
function getOperatorNameSerialMap(structure: Structure) {
|
||||
const map = new Map<string, number>()
|
||||
for (let i = 0, il = structure.units.length; i < il; ++i) {
|
||||
const name = structure.units[i].conformation.operator.name
|
||||
if (!map.has(name)) map.set(name, map.size)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
export function OperatorNameColorTheme(ctx: ThemeDataContext, props: PD.Values<OperatorNameColorThemeParams>): ColorTheme<OperatorNameColorThemeParams> {
|
||||
let color: LocationColor
|
||||
let legend: ScaleLegend | TableLegend | undefined
|
||||
|
||||
if (ctx.structure) {
|
||||
const operatorNameSerialMap = getOperatorNameSerialMap(ctx.structure.root)
|
||||
|
||||
const labelTable = Array.from(operatorNameSerialMap.keys())
|
||||
props.palette.params.valueLabel = (i: number) => labelTable[i]
|
||||
|
||||
const palette = getPalette(operatorNameSerialMap.size, props)
|
||||
legend = palette.legend
|
||||
|
||||
color = (location: Location): Color => {
|
||||
let serial: number | undefined = undefined
|
||||
if (StructureElement.Location.is(location)) {
|
||||
const name = location.unit.conformation.operator.name
|
||||
serial = operatorNameSerialMap.get(name)
|
||||
} else if (Link.isLocation(location)) {
|
||||
const name = location.aUnit.conformation.operator.name
|
||||
serial = operatorNameSerialMap.get(name)
|
||||
}
|
||||
return serial === undefined ? DefaultColor : palette.color(serial)
|
||||
}
|
||||
} else {
|
||||
color = () => DefaultColor
|
||||
}
|
||||
|
||||
return {
|
||||
factory: OperatorNameColorTheme,
|
||||
granularity: 'instance',
|
||||
color,
|
||||
props,
|
||||
description: Description,
|
||||
legend
|
||||
}
|
||||
}
|
||||
|
||||
export const OperatorNameColorThemeProvider: ColorTheme.Provider<OperatorNameColorThemeParams> = {
|
||||
label: 'Operator Name',
|
||||
factory: OperatorNameColorTheme,
|
||||
getParams: getOperatorNameColorThemeParams,
|
||||
defaultValues: PD.getDefaultValues(OperatorNameColorThemeParams),
|
||||
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure
|
||||
}
|
||||
@@ -27,7 +27,10 @@ export function getPolymerIdColorThemeParams(ctx: ThemeDataContext) {
|
||||
if (ctx.structure) {
|
||||
if (getPolymerAsymIdSerialMap(ctx.structure.root).size > 12) {
|
||||
params.palette.defaultValue.name = 'scale'
|
||||
params.palette.defaultValue.params = { list: 'red-yellow-blue' }
|
||||
params.palette.defaultValue.params = {
|
||||
...params.palette.defaultValue.params,
|
||||
list: 'red-yellow-blue'
|
||||
}
|
||||
}
|
||||
}
|
||||
return params
|
||||
@@ -88,6 +91,9 @@ export function PolymerIdColorTheme(ctx: ThemeDataContext, props: PD.Values<Poly
|
||||
const l = StructureElement.Location.create()
|
||||
const polymerAsymIdSerialMap = getPolymerAsymIdSerialMap(ctx.structure.root)
|
||||
|
||||
const labelTable = Array.from(polymerAsymIdSerialMap.keys())
|
||||
props.palette.params.valueLabel = (i: number) => labelTable[i]
|
||||
|
||||
const palette = getPalette(polymerAsymIdSerialMap.size, props)
|
||||
legend = palette.legend
|
||||
|
||||
|
||||
@@ -25,7 +25,10 @@ export function getPolymerIndexColorThemeParams(ctx: ThemeDataContext) {
|
||||
if (ctx.structure) {
|
||||
if (getPolymerChainCount(ctx.structure.root) > 12) {
|
||||
params.palette.defaultValue.name = 'scale'
|
||||
params.palette.defaultValue.params = { list: 'red-yellow-blue' }
|
||||
params.palette.defaultValue.params = {
|
||||
...params.palette.defaultValue.params,
|
||||
list: 'red-yellow-blue'
|
||||
}
|
||||
}
|
||||
}
|
||||
return params
|
||||
|
||||
@@ -25,7 +25,10 @@ export function getUnitIndexColorThemeParams(ctx: ThemeDataContext) {
|
||||
if (ctx.structure) {
|
||||
if (ctx.structure.root.units.length > 12) {
|
||||
params.palette.defaultValue.name = 'scale'
|
||||
params.palette.defaultValue.params = { list: 'red-yellow-blue' }
|
||||
params.palette.defaultValue.params = {
|
||||
...params.palette.defaultValue.params,
|
||||
list: 'red-yellow-blue'
|
||||
}
|
||||
}
|
||||
}
|
||||
return params
|
||||
|
||||
@@ -20,19 +20,28 @@ const DefaultGetPaletteProps = {
|
||||
}
|
||||
type GetPaletteProps = typeof DefaultGetPaletteProps
|
||||
|
||||
const LabelParams = {
|
||||
valueLabel: PD.Value((i: number) => `${i + 1}`, { isHidden: true }),
|
||||
minLabel: PD.Value('Start', { isHidden: true }),
|
||||
maxLabel: PD.Value('End', { isHidden: true }),
|
||||
}
|
||||
|
||||
export function getPaletteParams(props: Partial<GetPaletteProps> = {}) {
|
||||
const p = { ...DefaultGetPaletteProps, ...props }
|
||||
return {
|
||||
palette: PD.MappedStatic(p.type, {
|
||||
scale: PD.Group({
|
||||
...LabelParams,
|
||||
list: PD.ColorList<ColorListName>(p.scaleList, ColorListOptionsScale),
|
||||
}, { isFlat: true }),
|
||||
set: PD.Group({
|
||||
...LabelParams,
|
||||
list: PD.ColorList<ColorListName>(p.setList, ColorListOptionsSet),
|
||||
}, { isFlat: true }),
|
||||
generate: PD.Group({
|
||||
...LabelParams,
|
||||
...DistinctColorsParams,
|
||||
maxCount: PD.Numeric(75, { min: 1, max: 250, step: 1 })
|
||||
maxCount: PD.Numeric(75, { min: 1, max: 250, step: 1 }),
|
||||
}, { isFlat: true })
|
||||
}, {
|
||||
options: [
|
||||
@@ -57,9 +66,9 @@ export function getPalette(count: number, props: PaletteProps) {
|
||||
let legend: ScaleLegend | TableLegend | undefined
|
||||
|
||||
if (props.palette.name === 'scale') {
|
||||
const listOrName = props.palette.params.list
|
||||
const { list: listOrName, minLabel, maxLabel } = props.palette.params
|
||||
const domain: [number, number] = [0, count - 1]
|
||||
const scale = ColorScale.create({ listOrName, domain, minLabel: 'Start', maxLabel: 'End' })
|
||||
const scale = ColorScale.create({ listOrName, domain, minLabel, maxLabel })
|
||||
legend = scale.legend
|
||||
color = scale.color
|
||||
} else {
|
||||
@@ -71,8 +80,18 @@ export function getPalette(count: number, props: PaletteProps) {
|
||||
count = Math.min(count, props.palette.params.maxCount)
|
||||
colors = distinctColors(count, props.palette.params)
|
||||
}
|
||||
const { valueLabel } = props.palette.params
|
||||
const colorsLength = colors.length
|
||||
legend = TableLegend(colors.map((c, i) => [`${i + 1}`, c]))
|
||||
const table: [string, Color][] = []
|
||||
for (let i = 0; i < count; ++i) {
|
||||
const j = i % colorsLength
|
||||
if (table[j] === undefined) {
|
||||
table[j] = [valueLabel(i), colors[j]]
|
||||
} else {
|
||||
table[j][0] += `, ${valueLabel(i)}`
|
||||
}
|
||||
}
|
||||
legend = TableLegend(table)
|
||||
color = (i: number) => colors[i % colorsLength]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user