Compare commits

...

15 Commits

Author SHA1 Message Date
Alexander Rose
944d370c14 0.3.2 2019-10-09 16:59:24 -07:00
Alexander Rose
74f9aa6af6 package updates 2019-10-09 16:55:44 -07:00
Alexander Rose
c20c9c9917 fixed setting CollapsableControls default state 2019-10-09 16:54:15 -07:00
Alexander Rose
4801435d72 add controls to create image 2019-10-09 16:10:36 -07:00
Alexander Rose
33fd105ef7 add ImagePass 2019-10-09 16:09:43 -07:00
Alexander Rose
3ea3fb8984 support rendering with transparent background 2019-10-09 16:08:48 -07:00
Alexander Rose
b4bbc544ca 0.3.1 2019-10-07 17:27:35 -07:00
Alexander Rose
5f880e920b add focus button to StructureSelectionControls 2019-10-07 17:26:16 -07:00
Alexander Rose
bcce801dd7 ensure sequence markers are up-to-date 2019-10-07 16:10:21 -07:00
Alexander Rose
00f9dcee4a added StructureSymmetryMatesFromModel transform, fixes for findMatesRadius 2019-10-07 15:31:54 -07:00
Alexander Rose
505af2bc96 fix help for scrollFocus 2019-10-07 12:04:20 -07:00
Alexander Rose
c217aab5fc reduced default unicell cage thickness 2019-10-07 11:23:50 -07:00
Alexander Rose
5d5fd0028f added operator name & hkl color themes 2019-10-07 11:20:10 -07:00
Alexander Rose
c88693dfdd improved color theme legend with labels from data 2019-10-07 11:18:08 -07:00
David Sehnal
0a16ec1bd2 mol-plugin: added TextInput wrapper and RGB color input 2019-10-05 14:52:00 +02:00
41 changed files with 1050 additions and 135 deletions

28
package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -168,6 +168,7 @@ export const GlobalUniformSchema = {
uFogFar: UniformSpec('f'),
uFogColor: UniformSpec('v3'),
uTransparentBackground: UniformSpec('i'),
uPickingAlphaThreshold: UniformSpec('f'),
uInteriorDarkening: UniformSpec('f'),
}

View File

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

View File

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

View File

@@ -22,6 +22,7 @@ uniform vec3 uFogColor;
uniform float uAlpha;
uniform float uPickingAlphaThreshold;
uniform int uPickable;
uniform int uTransparentBackground;
uniform float uInteriorDarkening;
`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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