diff --git a/CHANGELOG.md b/CHANGELOG.md index 89f3efb66dd08c543258bff564743694ae0411bd..0243da86ce80494bd4c6294d680b9bc7c527df4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Note that since we don't clearly distinguish between a public and private interf - Add Charmm saccharide names - Treat missing occupancy column as occupany of 1 +- Add additional measurement controls: orientation (box, axes, ellipsoid) & plane (best fit) ## [v2.3.0] - 2021-09-06 diff --git a/src/mol-plugin-state/manager/structure/measurement.ts b/src/mol-plugin-state/manager/structure/measurement.ts index 4907eb3e7edf84d536604126b5811b722e8265ec..bbd3f14226a471b5c3bfb4c1c8cc8bd8ddffce0a 100644 --- a/src/mol-plugin-state/manager/structure/measurement.ts +++ b/src/mol-plugin-state/manager/structure/measurement.ts @@ -1,7 +1,8 @@ /** - * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> + * @author Alexander Rose <alexander.rose@weirdbyte.de> */ import { StructureElement } from '../../../mol-model/structure'; @@ -15,6 +16,7 @@ import { StatefulPluginComponent } from '../../component'; import { ParamDefinition as PD } from '../../../mol-util/param-definition'; import { MeasurementRepresentationCommonTextParams, LociLabelTextParams } from '../../../mol-repr/shape/loci/common'; import { LineParams } from '../../../mol-repr/structure/representation/line'; +import { Expression } from '../../../mol-script/language/expression'; export { StructureMeasurementManager }; @@ -35,6 +37,7 @@ export interface StructureMeasurementManagerState { angles: StructureMeasurementCell[], dihedrals: StructureMeasurementCell[], orientations: StructureMeasurementCell[], + planes: StructureMeasurementCell[], options: StructureMeasurementOptions } @@ -222,19 +225,25 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } }); } - async addOrientation(a: StructureElement.Loci) { - const cellA = this.plugin.helpers.substructureParent.get(a.structure); + async addOrientation(locis: StructureElement.Loci[]) { + const selections: { key: string, ref: string, groupId?: string, expression: Expression }[] = []; + const dependsOn: string[] = []; - if (!cellA) return; + for (let i = 0, il = locis.length; i < il; ++i) { + const l = locis[i]; + const cell = this.plugin.helpers.substructureParent.get(l.structure); + if (!cell) continue; - const dependsOn = [cellA.transform.ref]; + arraySetAdd(dependsOn, cell.transform.ref); + selections.push({ key: `l${i}`, ref: cell.transform.ref, expression: StructureElement.Loci.toExpression(l) }); + } + + if (selections.length === 0) return; const update = this.getGroup(); update .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, { - selections: [ - { key: 'a', ref: cellA.transform.ref, expression: StructureElement.Loci.toExpression(a) }, - ], + selections, isTransitive: true, label: 'Orientation' }, { dependsOn }) @@ -244,6 +253,34 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } }); } + async addPlane(locis: StructureElement.Loci[]) { + const selections: { key: string, ref: string, groupId?: string, expression: Expression }[] = []; + const dependsOn: string[] = []; + + for (let i = 0, il = locis.length; i < il; ++i) { + const l = locis[i]; + const cell = this.plugin.helpers.substructureParent.get(l.structure); + if (!cell) continue; + + arraySetAdd(dependsOn, cell.transform.ref); + selections.push({ key: `l${i}`, ref: cell.transform.ref, expression: StructureElement.Loci.toExpression(l) }); + } + + if (selections.length === 0) return; + + const update = this.getGroup(); + update + .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, { + selections, + isTransitive: true, + label: 'Plane' + }, { dependsOn }) + .apply(StateTransforms.Representation.StructureSelectionsPlane3D); + + const state = this.plugin.state.data; + await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } }); + } + private _empty: any[] = []; private getTransforms<T extends StateTransformer<A, B, any>, A extends PluginStateObject.Molecule.Structure.Selections, B extends StateObject>(transformer: T) { const state = this.plugin.state.data; @@ -259,13 +296,14 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu distances: this.getTransforms(StateTransforms.Representation.StructureSelectionsDistance3D), angles: this.getTransforms(StateTransforms.Representation.StructureSelectionsAngle3D), dihedrals: this.getTransforms(StateTransforms.Representation.StructureSelectionsDihedral3D), - orientations: this.getTransforms(StateTransforms.Representation.StructureSelectionsOrientation3D) + orientations: this.getTransforms(StateTransforms.Representation.StructureSelectionsOrientation3D), + planes: this.getTransforms(StateTransforms.Representation.StructureSelectionsPlane3D), }); if (updated) this.stateUpdated(); } constructor(private plugin: PluginContext) { - super({ labels: [], distances: [], angles: [], dihedrals: [], orientations: [], options: DefaultStructureMeasurementOptions }); + super({ labels: [], distances: [], angles: [], dihedrals: [], orientations: [], planes: [], options: DefaultStructureMeasurementOptions }); plugin.state.data.events.changed.subscribe(e => { if (e.inTransaction || plugin.behaviors.state.isAnimating.value) return; diff --git a/src/mol-plugin-state/transforms/helpers.ts b/src/mol-plugin-state/transforms/helpers.ts index 55ca620ae5581c1021a46947a0f2b66a63d44f60..9bdd857bd67f5149b7b4f7cb029a676fbc5437d6 100644 --- a/src/mol-plugin-state/transforms/helpers.ts +++ b/src/mol-plugin-state/transforms/helpers.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -10,6 +10,7 @@ import { LabelData } from '../../mol-repr/shape/loci/label'; import { OrientationData } from '../../mol-repr/shape/loci/orientation'; import { AngleData } from '../../mol-repr/shape/loci/angle'; import { DihedralData } from '../../mol-repr/shape/loci/dihedral'; +import { PlaneData } from '../../mol-repr/shape/loci/plane'; export function getDistanceDataFromStructureSelections(s: ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry>): DistanceData { const lociA = s[0].loci; @@ -38,6 +39,9 @@ export function getLabelDataFromStructureSelections(s: ReadonlyArray<PluginState } export function getOrientationDataFromStructureSelections(s: ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry>): OrientationData { - const loci = s[0].loci; - return { locis: [loci] }; + return { locis: s.map(v => v.loci) }; +} + +export function getPlaneDataFromStructureSelections(s: ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry>): PlaneData { + return { locis: s.map(v => v.loci) }; } \ No newline at end of file diff --git a/src/mol-plugin-state/transforms/representation.ts b/src/mol-plugin-state/transforms/representation.ts index 6c1d37b0fd74453bf47878dca70639d0ddc91756..1b257422f4ad930792ac663b4daf07dd5b4d7197 100644 --- a/src/mol-plugin-state/transforms/representation.ts +++ b/src/mol-plugin-state/transforms/representation.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> * @author Alexander Rose <alexander.rose@weirdbyte.de> @@ -28,7 +28,7 @@ import { BaseGeometry } from '../../mol-geo/geometry/base'; import { Script } from '../../mol-script/script'; import { UnitcellParams, UnitcellRepresentation, getUnitcellData } from '../../mol-repr/shape/model/unitcell'; import { DistanceParams, DistanceRepresentation } from '../../mol-repr/shape/loci/distance'; -import { getDistanceDataFromStructureSelections, getLabelDataFromStructureSelections, getOrientationDataFromStructureSelections, getAngleDataFromStructureSelections, getDihedralDataFromStructureSelections } from './helpers'; +import { getDistanceDataFromStructureSelections, getLabelDataFromStructureSelections, getOrientationDataFromStructureSelections, getAngleDataFromStructureSelections, getDihedralDataFromStructureSelections, getPlaneDataFromStructureSelections } from './helpers'; import { LabelParams, LabelRepresentation } from '../../mol-repr/shape/loci/label'; import { OrientationRepresentation, OrientationParams } from '../../mol-repr/shape/loci/orientation'; import { AngleParams, AngleRepresentation } from '../../mol-repr/shape/loci/angle'; @@ -40,6 +40,7 @@ import { Mesh } from '../../mol-geo/geometry/mesh/mesh'; import { getBoxMesh } from './shape'; import { Shape } from '../../mol-model/shape'; import { Box3D } from '../../mol-math/geometry'; +import { PlaneParams, PlaneRepresentation } from '../../mol-repr/shape/loci/plane'; export { StructureRepresentation3D }; export { ExplodeStructureRepresentation3D }; @@ -986,4 +987,37 @@ const StructureSelectionsOrientation3D = PluginStateTransform.BuiltIn({ return StateTransformer.UpdateResult.Updated; }); }, +}); + +export { StructureSelectionsPlane3D }; +type StructureSelectionsPlane3D = typeof StructureSelectionsPlane3D +const StructureSelectionsPlane3D = PluginStateTransform.BuiltIn({ + name: 'structure-selections-plane-3d', + display: '3D Plane', + from: SO.Molecule.Structure.Selections, + to: SO.Shape.Representation3D, + params: () => ({ + ...PlaneParams, + }) +})({ + canAutoUpdate({ oldParams, newParams }) { + return true; + }, + apply({ a, params }, plugin: PluginContext) { + return Task.create('Structure Plane', async ctx => { + const data = getPlaneDataFromStructureSelections(a.data); + const repr = PlaneRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => PlaneParams); + await repr.createOrUpdate(params, data).runInContext(ctx); + return new SO.Shape.Representation3D({ repr, sourceData: data }, { label: `Plane` }); + }); + }, + update({ a, b, oldParams, newParams }, plugin: PluginContext) { + return Task.create('Structure Plane', async ctx => { + const props = { ...b.data.repr.props, ...newParams }; + const data = getPlaneDataFromStructureSelections(a.data); + await b.data.repr.createOrUpdate(props, data).runInContext(ctx); + b.data.sourceData = data; + return StateTransformer.UpdateResult.Updated; + }); + }, }); \ No newline at end of file diff --git a/src/mol-plugin-ui/structure/measurements.tsx b/src/mol-plugin-ui/structure/measurements.tsx index b1cdfb2a33ae1bfa5b289a29e7f79959bd4b36cd..29763a3cfe262d53a12b4a2ecbfbf8b833caf3f1 100644 --- a/src/mol-plugin-ui/structure/measurements.tsx +++ b/src/mol-plugin-ui/structure/measurements.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> * @author David Sehnal <david.sehnal@gmail.com> @@ -15,7 +15,8 @@ import { AngleData } from '../../mol-repr/shape/loci/angle'; import { DihedralData } from '../../mol-repr/shape/loci/dihedral'; import { DistanceData } from '../../mol-repr/shape/loci/distance'; import { LabelData } from '../../mol-repr/shape/loci/label'; -import { angleLabel, dihedralLabel, distanceLabel, lociLabel } from '../../mol-theme/label'; +import { OrientationData } from '../../mol-repr/shape/loci/orientation'; +import { angleLabel, dihedralLabel, distanceLabel, lociLabel, structureElementLociLabelMany } from '../../mol-theme/label'; import { FiniteArray } from '../../mol-util/type-helpers'; import { CollapsableControls, PurePluginUIComponent } from '../base'; import { ActionMenu } from '../controls/action-menu'; @@ -67,6 +68,8 @@ export class MeasurementList extends PurePluginUIComponent { {this.renderGroup(measurements.distances, 'Distances')} {this.renderGroup(measurements.angles, 'Angles')} {this.renderGroup(measurements.dihedrals, 'Dihedrals')} + {this.renderGroup(measurements.orientations, 'Orientations')} + {this.renderGroup(measurements.planes, 'Planes')} </div>; } } @@ -108,13 +111,31 @@ export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boo this.plugin.managers.structure.measurement.addLabel(loci[0].loci); } + addOrientation = () => { + const locis: StructureElement.Loci[] = []; + this.plugin.managers.structure.selection.entries.forEach(v => { + locis.push(v.selection); + }); + this.plugin.managers.structure.measurement.addOrientation(locis); + } + + addPlane = () => { + const locis: StructureElement.Loci[] = []; + this.plugin.managers.structure.selection.entries.forEach(v => { + locis.push(v.selection); + }); + this.plugin.managers.structure.measurement.addPlane(locis); + } + get actions(): ActionMenu.Items { const history = this.selection.additionsHistory; const ret: ActionMenu.Item[] = [ - { kind: 'item', label: `Label ${history.length === 0 ? ' (1 selection required)' : ' (1st selection)'}`, value: this.addLabel, disabled: history.length === 0 }, - { kind: 'item', label: `Distance ${history.length < 2 ? ' (2 selections required)' : ' (top 2 selections)'}`, value: this.measureDistance, disabled: history.length < 2 }, - { kind: 'item', label: `Angle ${history.length < 3 ? ' (3 selections required)' : ' (top 3 selections)'}`, value: this.measureAngle, disabled: history.length < 3 }, - { kind: 'item', label: `Dihedral ${history.length < 4 ? ' (4 selections required)' : ' (top 4 selections)'}`, value: this.measureDihedral, disabled: history.length < 4 }, + { kind: 'item', label: `Label ${history.length === 0 ? ' (1 selection item required)' : ' (1st selection item)'}`, value: this.addLabel, disabled: history.length === 0 }, + { kind: 'item', label: `Distance ${history.length < 2 ? ' (2 selection items required)' : ' (top 2 selection items)'}`, value: this.measureDistance, disabled: history.length < 2 }, + { kind: 'item', label: `Angle ${history.length < 3 ? ' (3 selection items required)' : ' (top 3 items)'}`, value: this.measureAngle, disabled: history.length < 3 }, + { kind: 'item', label: `Dihedral ${history.length < 4 ? ' (4 selection items required)' : ' (top 4 selection items)'}`, value: this.measureDihedral, disabled: history.length < 4 }, + { kind: 'item', label: `Orientation ${history.length === 0 ? ' (selection required)' : ' (current selection)'}`, value: this.addOrientation, disabled: history.length === 0 }, + { kind: 'item', label: `Plane ${history.length === 0 ? ' (selection required)' : ' (current selection)'}`, value: this.addPlane, disabled: history.length === 0 }, ]; return ret; } @@ -219,7 +240,7 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen } get selections() { - return this.props.cell.obj?.data.sourceData as Partial<DistanceData & AngleData & DihedralData & LabelData> | undefined; + return this.props.cell.obj?.data.sourceData as Partial<DistanceData & AngleData & DihedralData & LabelData & OrientationData> | undefined; } delete = () => { @@ -266,6 +287,7 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen if (selections.pairs) return selections.pairs[0].loci; if (selections.triples) return selections.triples[0].loci; if (selections.quads) return selections.quads[0].loci; + if (selections.locis) return selections.locis; return []; } @@ -277,6 +299,7 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen if (selections.pairs) return distanceLabel(selections.pairs[0], { condensed: true, unitLabel: this.plugin.managers.structure.measurement.state.options.distanceUnitLabel }); if (selections.triples) return angleLabel(selections.triples[0], { condensed: true }); if (selections.quads) return dihedralLabel(selections.quads[0], { condensed: true }); + if (selections.locis) return structureElementLociLabelMany(selections.locis, { countsOnly: true }); return '<empty>'; } diff --git a/src/mol-repr/shape/loci/orientation.ts b/src/mol-repr/shape/loci/orientation.ts index 4a5b120ec8acf1b73722a4c90d56db084290672a..acccf41841b8847f183aac2184f6f18f76a242c7 100644 --- a/src/mol-repr/shape/loci/orientation.ts +++ b/src/mol-repr/shape/loci/orientation.ts @@ -1,10 +1,9 @@ /** - * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { Loci } from '../../../mol-model/loci'; import { RuntimeContext } from '../../../mol-task'; import { ParamDefinition as PD } from '../../../mol-util/param-definition'; import { ColorNames } from '../../../mol-util/color/names'; @@ -13,16 +12,17 @@ import { Representation, RepresentationParamsGetter, RepresentationContext } fro import { Shape } from '../../../mol-model/shape'; import { Mesh } from '../../../mol-geo/geometry/mesh/mesh'; import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder'; -import { lociLabel } from '../../../mol-theme/label'; +import { structureElementLociLabelMany } from '../../../mol-theme/label'; import { addAxes } from '../../../mol-geo/geometry/mesh/builder/axes'; import { addOrientedBox } from '../../../mol-geo/geometry/mesh/builder/box'; import { addEllipsoid } from '../../../mol-geo/geometry/mesh/builder/ellipsoid'; import { Axes3D } from '../../../mol-math/geometry'; import { Vec3 } from '../../../mol-math/linear-algebra'; import { MarkerActions } from '../../../mol-util/marker-action'; +import { StructureElement } from '../../../mol-model/structure'; export interface OrientationData { - locis: Loci[] + locis: StructureElement.Loci[] } const SharedParams = { @@ -57,6 +57,7 @@ const OrientationVisuals = { export const OrientationParams = { ...AxesParams, ...BoxParams, + ...EllipsoidParams, visuals: PD.MultiSelect(['box'], PD.objectToOptions(OrientationVisuals)), color: PD.Color(ColorNames.orange), scale: PD.Numeric(2, { min: 0.1, max: 5, step: 0.1 }) @@ -66,88 +67,74 @@ export type OrientationProps = PD.Values<OrientationParams> // -function orientationLabel(loci: Loci) { - const label = lociLabel(loci, { countsOnly: true }); +function getAxesName(locis: StructureElement.Loci[]) { + const label = structureElementLociLabelMany(locis, { countsOnly: true }); return `Principal Axes of ${label}`; } -function getOrientationName(data: OrientationData) { - return data.locis.length === 1 ? orientationLabel(data.locis[0]) : `${data.locis.length} Orientations`; -} - -// - function buildAxesMesh(data: OrientationData, props: OrientationProps, mesh?: Mesh): Mesh { const state = MeshBuilder.createState(256, 128, mesh); - for (let i = 0, il = data.locis.length; i < il; ++i) { - const principalAxes = Loci.getPrincipalAxes(data.locis[i]); - if (principalAxes) { - state.currentGroup = i; - addAxes(state, principalAxes.momentsAxes, props.scale, 2, 20); - } - } + const principalAxes = StructureElement.Loci.getPrincipalAxesMany(data.locis); + + state.currentGroup = 0; + addAxes(state, principalAxes.momentsAxes, props.scale, 2, 20); return MeshBuilder.getMesh(state); } function getAxesShape(ctx: RuntimeContext, data: OrientationData, props: OrientationProps, shape?: Shape<Mesh>) { const mesh = buildAxesMesh(data, props, shape && shape.geometry); - const name = getOrientationName(data); - const getLabel = function (groupId: number) { - return orientationLabel(data.locis[groupId]); - }; - return Shape.create(name, data, mesh, () => props.color, () => 1, getLabel); + const name = getAxesName(data.locis); + return Shape.create(name, data, mesh, () => props.color, () => 1, () => name); } // +function getBoxName(locis: StructureElement.Loci[]) { + const label = structureElementLociLabelMany(locis, { countsOnly: true }); + return `Oriented Box of ${label}`; +} + function buildBoxMesh(data: OrientationData, props: OrientationProps, mesh?: Mesh): Mesh { const state = MeshBuilder.createState(256, 128, mesh); - for (let i = 0, il = data.locis.length; i < il; ++i) { - const principalAxes = Loci.getPrincipalAxes(data.locis[i]); - if (principalAxes) { - state.currentGroup = i; - addOrientedBox(state, principalAxes.boxAxes, props.scale, 2, 20); - } - } + const principalAxes = StructureElement.Loci.getPrincipalAxesMany(data.locis); + + state.currentGroup = 0; + addOrientedBox(state, principalAxes.boxAxes, props.scale, 2, 20); return MeshBuilder.getMesh(state); } function getBoxShape(ctx: RuntimeContext, data: OrientationData, props: OrientationProps, shape?: Shape<Mesh>) { const mesh = buildBoxMesh(data, props, shape && shape.geometry); - const name = getOrientationName(data); - const getLabel = function (groupId: number) { - return orientationLabel(data.locis[groupId]); - }; - return Shape.create(name, data, mesh, () => props.color, () => 1, getLabel); + const name = getBoxName(data.locis); + return Shape.create(name, data, mesh, () => props.color, () => 1, () => name); } // +function getEllipsoidName(locis: StructureElement.Loci[]) { + const label = structureElementLociLabelMany(locis, { countsOnly: true }); + return `Oriented Ellipsoid of ${label}`; +} + function buildEllipsoidMesh(data: OrientationData, props: OrientationProps, mesh?: Mesh): Mesh { const state = MeshBuilder.createState(256, 128, mesh); - for (let i = 0, il = data.locis.length; i < il; ++i) { - const principalAxes = Loci.getPrincipalAxes(data.locis[i]); - if (principalAxes) { - const axes = principalAxes.boxAxes; - const { origin, dirA, dirB } = axes; - const size = Axes3D.size(Vec3(), axes); - Vec3.scale(size, size, 0.5); - const radiusScale = Vec3.create(size[2], size[1], size[0]); - - state.currentGroup = i; - addEllipsoid(state, origin, dirA, dirB, radiusScale, 2); - } - } + const principalAxes = StructureElement.Loci.getPrincipalAxesMany(data.locis); + + const axes = principalAxes.boxAxes; + const { origin, dirA, dirB } = axes; + const size = Axes3D.size(Vec3(), axes); + Vec3.scale(size, size, 0.5); + const radiusScale = Vec3.create(size[2], size[1], size[0]); + + state.currentGroup = 0; + addEllipsoid(state, origin, dirA, dirB, radiusScale, 2); return MeshBuilder.getMesh(state); } function getEllipsoidShape(ctx: RuntimeContext, data: OrientationData, props: OrientationProps, shape?: Shape<Mesh>) { const mesh = buildEllipsoidMesh(data, props, shape && shape.geometry); - const name = getOrientationName(data); - const getLabel = function (groupId: number) { - return orientationLabel(data.locis[groupId]); - }; - return Shape.create(name, data, mesh, () => props.color, () => 1, getLabel); + const name = getEllipsoidName(data.locis); + return Shape.create(name, data, mesh, () => props.color, () => 1, () => name); } // diff --git a/src/mol-repr/shape/loci/plane.ts b/src/mol-repr/shape/loci/plane.ts new file mode 100644 index 0000000000000000000000000000000000000000..e33213648e336d3d452d20d0fc7bd7fd9638493f --- /dev/null +++ b/src/mol-repr/shape/loci/plane.ts @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { RuntimeContext } from '../../../mol-task'; +import { ParamDefinition as PD } from '../../../mol-util/param-definition'; +import { ColorNames } from '../../../mol-util/color/names'; +import { ShapeRepresentation } from '../representation'; +import { Representation, RepresentationParamsGetter, RepresentationContext } from '../../representation'; +import { Shape } from '../../../mol-model/shape'; +import { Mesh } from '../../../mol-geo/geometry/mesh/mesh'; +import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder'; +import { structureElementLociLabelMany } from '../../../mol-theme/label'; +import { Mat4, Vec3 } from '../../../mol-math/linear-algebra'; +import { MarkerActions } from '../../../mol-util/marker-action'; +import { Plane } from '../../../mol-geo/primitive/plane'; +import { StructureElement } from '../../../mol-model/structure'; +import { Axes3D } from '../../../mol-math/geometry'; + +export interface PlaneData { + locis: StructureElement.Loci[] +} + +const _PlaneParams = { + ...Mesh.Params, + color: PD.Color(ColorNames.orange), + scale: PD.Numeric(1, { min: 0.1, max: 10, step: 0.1 }) +}; +type _PlaneParams = typeof _PlaneParams + +const PlaneVisuals = { + 'plane': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<PlaneData, _PlaneParams>) => ShapeRepresentation(getPlaneShape, Mesh.Utils), +}; + +export const PlaneParams = { + ..._PlaneParams, + visuals: PD.MultiSelect(['plane'], PD.objectToOptions(PlaneVisuals)), + color: PD.Color(ColorNames.orange), + scale: PD.Numeric(1, { min: 0.1, max: 5, step: 0.1 }) +}; +export type PlaneParams = typeof PlaneParams +export type PlaneProps = PD.Values<PlaneParams> + +// + +function getPlaneName(locis: StructureElement.Loci[]) { + const label = structureElementLociLabelMany(locis, { countsOnly: true }); + return `Best Fit Plane of ${label}`; +} + +const tmpMat = Mat4(); +const tmpV = Vec3(); +function buildPlaneMesh(data: PlaneData, props: PlaneProps, mesh?: Mesh): Mesh { + const state = MeshBuilder.createState(256, 128, mesh); + const principalAxes = StructureElement.Loci.getPrincipalAxesMany(data.locis); + const axes = principalAxes.boxAxes; + const plane = Plane(); + + Vec3.add(tmpV, axes.origin, axes.dirC); + Mat4.targetTo(tmpMat, tmpV, axes.origin, axes.dirB); + Mat4.scale(tmpMat, tmpMat, Axes3D.size(tmpV, axes)); + Mat4.scaleUniformly(tmpMat, tmpMat, props.scale); + Mat4.setTranslation(tmpMat, axes.origin); + + state.currentGroup = 0; + MeshBuilder.addPrimitive(state, tmpMat, plane); + MeshBuilder.addPrimitiveFlipped(state, tmpMat, plane); + return MeshBuilder.getMesh(state); +} + +function getPlaneShape(ctx: RuntimeContext, data: PlaneData, props: PlaneProps, shape?: Shape<Mesh>) { + const mesh = buildPlaneMesh(data, props, shape && shape.geometry); + const name = getPlaneName(data.locis); + return Shape.create(name, data, mesh, () => props.color, () => 1, () => name); +} + +// + +export type PlaneRepresentation = Representation<PlaneData, PlaneParams> +export function PlaneRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<PlaneData, PlaneParams>): PlaneRepresentation { + const repr = Representation.createMulti('Plane', ctx, getParams, Representation.StateBuilder, PlaneVisuals as unknown as Representation.Def<PlaneData, PlaneParams>); + repr.setState({ markerActions: MarkerActions.Highlighting }); + return repr; +} \ No newline at end of file