Skip to content
Snippets Groups Projects
Commit 9e8951da authored by Alexander Rose's avatar Alexander Rose
Browse files

added scene-labels behavior

parent 27c485ac
No related branches found
No related tags found
No related merge requests found
...@@ -15,6 +15,7 @@ import * as DynamicRepresentation from './behavior/dynamic/representation' ...@@ -15,6 +15,7 @@ import * as DynamicRepresentation from './behavior/dynamic/representation'
import * as DynamicCamera from './behavior/dynamic/camera' import * as DynamicCamera from './behavior/dynamic/camera'
import * as DynamicCustomProps from './behavior/dynamic/custom-props' import * as DynamicCustomProps from './behavior/dynamic/custom-props'
import * as DynamicAnimation from './behavior/dynamic/animation' import * as DynamicAnimation from './behavior/dynamic/animation'
import * as DynamicLabels from './behavior/dynamic/labels'
export const BuiltInPluginBehaviors = { export const BuiltInPluginBehaviors = {
State: StaticState, State: StaticState,
...@@ -27,5 +28,6 @@ export const PluginBehaviors = { ...@@ -27,5 +28,6 @@ export const PluginBehaviors = {
Representation: DynamicRepresentation, Representation: DynamicRepresentation,
Camera: DynamicCamera, Camera: DynamicCamera,
CustomProps: DynamicCustomProps, CustomProps: DynamicCustomProps,
Animation: DynamicAnimation Animation: DynamicAnimation,
Labels: DynamicLabels
} }
\ No newline at end of file
...@@ -95,7 +95,7 @@ namespace PluginBehavior { ...@@ -95,7 +95,7 @@ namespace PluginBehavior {
for (const s of this.subs) s.unsubscribe(); for (const s of this.subs) s.unsubscribe();
this.subs = []; this.subs = [];
} }
update(params: P): boolean { update(params: P): boolean | Promise<boolean> {
if (shallowEqual(params, this.params)) return false; if (shallowEqual(params, this.params)) return false;
this.params = params; this.params = params;
return true; return true;
......
/**
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { PluginContext } from 'mol-plugin/context';
import { PluginBehavior } from '../behavior';
import { ParamDefinition as PD } from 'mol-util/param-definition'
import { Mat4, Vec3 } from 'mol-math/linear-algebra';
import { PluginStateObject as SO, PluginStateObject } from '../../state/objects';
import { StateSelection } from 'mol-state/state/selection';
import { StateObjectCell, State } from 'mol-state';
import { RuntimeContext } from 'mol-task';
import { Shape } from 'mol-model/shape';
import { Text } from 'mol-geo/geometry/text/text';
import { ShapeRepresentation } from 'mol-repr/shape/representation';
import { ColorNames } from 'mol-util/color/tables';
import { TextBuilder } from 'mol-geo/geometry/text/text-builder';
import { Unit, StructureElement, StructureProperties } from 'mol-model/structure';
import { SetUtils } from 'mol-util/set';
import { arrayEqual } from 'mol-util';
import { MoleculeType } from 'mol-model/structure/model/types';
// TODO
// - support more object types than structures
// - tether label to the element nearest to the bounding sphere center
// - [Started] multiple levels of labels: structure, polymer, ligand
// - show structure/unit label only when there is a representation with sufficient overlap
// - support highlighting
// - better support saccharides (use data available after re-mediation)
// - size based on min bbox dimension (to avoid huge labels for very long but narrow polymers)
// - fixed size labels (invariant to zoom) [needs feature in text geo]
// - ??? max label length
// - ??? multi line labels [needs feature in text geo]
// - ??? use prevalent (how to define) color of representations of a structure to color the label
// - completely different approach (render not as 3d objects): overlay free layout in screenspace with occlusion info from bboxes
export type SceneLabelsLevels = 'structure' | 'polymer' | 'ligand'
export const SceneLabelsParams = {
...Text.Params,
background: PD.Boolean(true),
backgroundMargin: PD.Numeric(0.2, { min: 0, max: 1, step: 0.01 }),
backgroundColor: PD.Color(ColorNames.snow),
backgroundOpacity: PD.Numeric(0.9, { min: 0, max: 1, step: 0.01 }),
levels: PD.MultiSelect([] as SceneLabelsLevels[], [
['structure', 'structure'], ['polymer', 'polymer'], ['ligand', 'ligand']
] as [SceneLabelsLevels, string][]),
}
export type SceneLabelsParams = typeof SceneLabelsParams
export type SceneLabelsProps = PD.Values<typeof SceneLabelsParams>
interface LabelsData {
transforms: Mat4[]
texts: string[]
positions: Vec3[]
sizes: number[]
depths: number[]
}
function getLabelsText(data: LabelsData, props: PD.Values<Text.Params>, text?: Text) {
const { texts, positions, depths } = data
const textBuilder = TextBuilder.create(props, texts.length * 10, texts.length * 10 / 2, text)
for (let i = 0, il = texts.length; i < il; ++i) {
const p = positions[i]
textBuilder.add(texts[i], p[0], p[1], p[2], depths[i], i)
}
return textBuilder.getText()
}
export const SceneLabels = PluginBehavior.create<SceneLabelsProps>({
name: 'scene-labels',
display: { name: 'Scene Labels', group: 'Labels' },
ctor: class extends PluginBehavior.Handler<SceneLabelsProps> {
private data: LabelsData = {
transforms: [Mat4.identity()],
texts: [],
positions: [],
sizes: [],
depths: []
}
private repr: ShapeRepresentation<LabelsData, Text, SceneLabelsParams>
private geo = Text.createEmpty()
private structures = new Set<SO.Molecule.Structure>()
constructor(protected ctx: PluginContext, protected params: SceneLabelsProps) {
super(ctx, params)
this.repr = ShapeRepresentation(this.getLabelsShape, Text.Utils)
ctx.events.state.object.created.subscribe(this.triggerUpdate)
ctx.events.state.object.removed.subscribe(this.triggerUpdate)
ctx.events.state.object.updated.subscribe(this.triggerUpdate)
ctx.events.state.cell.stateUpdated.subscribe(this.triggerUpdate)
}
private triggerUpdate = async () => {
await this.update(this.params)
}
private getColor = () => ColorNames.dimgrey
private getSize = (groupId: number) => this.data.sizes[groupId]
private getLabel = () => ''
private getLabelsShape = (ctx: RuntimeContext, data: LabelsData, props: SceneLabelsProps, shape?: Shape<Text>) => {
this.geo = getLabelsText(data, props, this.geo)
return Shape.create('Scene Labels', this.geo, this.getColor, this.getSize, this.getLabel, data.transforms)
}
/** Update structures to be labeled, returns true if changed */
private updateStructures(p: SceneLabelsProps) {
const state = this.ctx.state.dataState
const structures = state.select(q => q.rootsOfType(PluginStateObject.Molecule.Structure));
const rootStructures = new Set<SO.Molecule.Structure>()
for (const s of structures) {
const rootStructure = getRootStructure(s, state)
if (!rootStructure || !SO.Molecule.Structure.is(rootStructure.obj)) continue
if (!state.cellStates.get(s.transform.ref).isHidden) {
rootStructures.add(rootStructure.obj)
}
}
if (!SetUtils.areEqual(rootStructures, this.structures)) {
this.structures = rootStructures
return true
} else {
return false
}
}
private updateLabels(p: SceneLabelsProps) {
const l = StructureElement.create()
const { texts, positions, sizes, depths } = this.data
texts.length = 0
positions.length = 0
sizes.length = 0
depths.length = 0
this.structures.forEach(structure => {
if (p.levels.includes('structure')) {
texts.push(`${structure.data.model.label}`)
positions.push(structure.data.boundary.sphere.center)
sizes.push(structure.data.boundary.sphere.radius / 10)
depths.push(structure.data.boundary.sphere.radius)
}
for (let i = 0, il = structure.data.units.length; i < il; ++i) {
let label = ''
const u = structure.data.units[i]
l.unit = u
l.element = u.elements[0]
if (p.levels.includes('polymer') && u.polymerElements.length) {
label = `${StructureProperties.entity.pdbx_description(l)} (${getAsymId(u)(l)})`
}
if (p.levels.includes('ligand') && !u.polymerElements.length) {
const compId = StructureProperties.residue.label_comp_id(l)
const chemComp = u.model.properties.chemicalComponentMap.get(compId)
const moleculeType = chemComp ? chemComp.moleculeType : MoleculeType.unknown
if (moleculeType === MoleculeType.other || moleculeType === MoleculeType.saccharide) {
label = `${StructureProperties.entity.pdbx_description(l)} (${getAsymId(u)(l)})`
}
}
if (label) {
texts.push(label)
const { center, radius } = u.lookup3d.boundary.sphere
const transformedCenter = Vec3.transformMat4(Vec3.zero(), center, u.conformation.operator.matrix)
positions.push(transformedCenter)
sizes.push(Math.max(2, radius / 10))
depths.push(radius)
}
}
})
}
register(): void { }
async update(p: SceneLabelsProps) {
// console.log('update')
let updated = false
if (this.updateStructures(p) || !arrayEqual(this.params.levels, p.levels)) {
// console.log('update with data')
this.updateLabels(p)
await this.repr.createOrUpdate(p, this.data).run()
updated = true
} else if (!PD.areEqual(SceneLabelsParams, this.params, p)) {
// console.log('update props only')
await this.repr.createOrUpdate(p).run()
updated = true
}
if (updated) {
Object.assign(this.params, p)
this.ctx.canvas3d.add(this.repr)
}
return updated;
}
unregister() {
}
},
params: () => SceneLabelsParams
});
//
function getRootStructure(root: StateObjectCell, state: State) {
let parent: StateObjectCell | undefined
while (true) {
const _parent = StateSelection.findAncestorOfType(state.tree, state.cells, root.transform.ref, [PluginStateObject.Molecule.Structure])
if (_parent) {
parent = _parent
root = _parent
} else {
break
}
}
return parent ? parent :
SO.Molecule.Structure.is(root.obj) ? root : undefined
}
function getAsymId(unit: Unit): StructureElement.Property<string> {
switch (unit.kind) {
case Unit.Kind.Atomic:
return StructureProperties.chain.auth_asym_id
case Unit.Kind.Spheres:
case Unit.Kind.Gaussians:
return StructureProperties.coarse.asym_id
}
}
\ No newline at end of file
...@@ -14,6 +14,8 @@ import { PluginSpec } from './spec'; ...@@ -14,6 +14,8 @@ import { PluginSpec } from './spec';
import { DownloadStructure, CreateComplexRepresentation, OpenStructure } from './state/actions/basic'; import { DownloadStructure, CreateComplexRepresentation, OpenStructure } from './state/actions/basic';
import { StateTransforms } from './state/transforms'; import { StateTransforms } from './state/transforms';
import { PluginBehaviors } from './behavior'; import { PluginBehaviors } from './behavior';
import { ParamDefinition as PD } from 'mol-util/param-definition'
import { SceneLabelsParams } from './behavior/dynamic/labels';
function getParam(name: string, regex: string): string { function getParam(name: string, regex: string): string {
let r = new RegExp(`${name}=(${regex})[&]?`, 'i'); let r = new RegExp(`${name}=(${regex})[&]?`, 'i');
...@@ -38,6 +40,7 @@ const DefaultSpec: PluginSpec = { ...@@ -38,6 +40,7 @@ const DefaultSpec: PluginSpec = {
PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider), PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider),
PluginSpec.Behavior(PluginBehaviors.Camera.FocusLociOnSelect, { minRadius: 20, extraRadius: 4 }), PluginSpec.Behavior(PluginBehaviors.Camera.FocusLociOnSelect, { minRadius: 20, extraRadius: 4 }),
PluginSpec.Behavior(PluginBehaviors.Animation.StructureAnimation, { rotate: false, rotateValue: 0, explode: false, explodeValue: 0 }), PluginSpec.Behavior(PluginBehaviors.Animation.StructureAnimation, { rotate: false, rotateValue: 0, explode: false, explodeValue: 0 }),
PluginSpec.Behavior(PluginBehaviors.Labels.SceneLabels, { ...PD.getDefaultValues(SceneLabelsParams) }), // TODO how to properly call PluginBehaviors.Labels.SceneLabels.definition.params()
PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true }), PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true }),
PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBAssemblySymmetry, { autoAttach: true }), PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBAssemblySymmetry, { autoAttach: true }),
] ]
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment