Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/rotate selection and pivot #909

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add selection-centered rotation & pivot point
This adds a `pivot` property to Component, with a `setPivot` method.

It also arranges for StructureComponents to optionally pass the
current selection to Component.updateMatrix when it gets the
component's center.

StructureComponents now can set their centering mode to "selection" or
"component". This makes components able to rotate around their
selection's center, not just the complete structure's center.

To change the centering mode, call new method
`StructureComponent.setCenterMode()`. The default is set to
"selection", which is new & improved behavior. Set it to "component"
to get the old behavior.

This also adds a pivot.js example to demonstrate the pivot feature in
a couple of different ways, as well as demonstrate selection-based
centering, showing how to update the transform when changing selection
to avoid molecules jumping around. (We could do that in the core code,
but since it uses Component.transform, users who are already using
that would be impacted -- so probably best to just show it in an
example and users can adapt it to suit.)
  • Loading branch information
garyo committed Jan 26, 2022
commit 5356a7a432c8bc3c46af97b3a67079553fe6e2f2
111 changes: 111 additions & 0 deletions examples/scripts/test/pivot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Handle window resizing
window.addEventListener('resize', function () {
stage.handleResize()
}, false)

var newDiv = document.getElementById('viewport').appendChild(document.createElement('div'))
newDiv.setAttribute('style', 'position: absolute; top: 0; left: 20px')
newDiv.innerHTML = `<div class="controls"><h3>Example of Setting Pivot Point</h3>
<p class="credit">Adjust pivot point X with slider, then use Ctrl+Shift+Left-drag to rotate comp.<br>
Click on an atom to set pivot point to that atom (instead of center).<br>
To test center modes, set comp selection to (e.g.) "1-20" and rotate it.
<br>
Things to look for: in selection-center mode, changing the selection<br>
shouldn't move the molecule even if it's been translated, scaled, and<br>
rotated. In that mode, the molecule should scale and rotate about the<br>
selection's center.
</p>
<p>Pivot point X/Y/Z: [<span id="pivotx"></span>, <span id="pivoty"></span>, <span id="pivotz"></span>]</p>
<input type="range" min="-10" max="10" step="0.1" value="0" id="pivotSliderX" class="mySlider"></input><br>
<input type="range" min="-10" max="10" step="0.1" value="0" id="pivotSliderY" class="mySlider"></input><br>
<input type="range" min="-10" max="10" step="0.1" value="0" id="pivotSliderZ" class="mySlider"></input>
<p>Current center mode: <span id="center-mode">selection</span></p>
<input id="toggle-mode-button" type="button" value="Toggle center mode"></input>
</div>`

var comp = null

var pivot = {x: 0, y: 0, z: 0}

const tmpMat4 = new NGL.Matrix4()

function num2str (x, precision) {
if (x >= 0) { return ' ' + x.toFixed(precision) } else { return x.toFixed(precision) }
}

function matrix4ToString (matrix, prefix = '', prec = 2) {
const m = matrix.elements
return `[${num2str(m[0], prec)} ${num2str(m[4], prec)} ${num2str(m[8], prec)} ${num2str(m[12], prec)}\n` +
`${prefix} ${num2str(m[1], prec)} ${num2str(m[5], prec)} ${num2str(m[9], prec)} ${num2str(m[13], prec)}\n` +
`${prefix} ${num2str(m[2], prec)} ${num2str(m[6], prec)} ${num2str(m[10], prec)} ${num2str(m[14], prec)}\n` +
`${prefix} ${num2str(m[3], prec)} ${num2str(m[7], prec)} ${num2str(m[11], prec)} ${num2str(m[15], prec)}]`
}

/** Set mode, or toggle if mode is undefined */
function toggleCenterMode (mode) {
if (mode === 'component' || mode === 'selection') { comp.centerMode = mode } else if (comp.centerMode === 'selection') { comp.centerMode = 'component' } else { comp.centerMode = 'selection' }
document.getElementById('center-mode').innerHTML = comp.centerMode
console.log(`Set center mode to ${comp.centerMode}`)
}
document.getElementById('toggle-mode-button')
.addEventListener('click', toggleCenterMode)

function updatePivotUI () {
for (const name of ['x', 'y', 'z']) {
document.getElementById('pivot' + name).innerHTML = pivot[name].toFixed(2)
}
}
updatePivotUI()

// Set up listener for dragging slider: set pivot point from slider values
for (const name of ['x', 'y', 'z']) {
document.getElementById('pivotSlider' + name.toUpperCase()).addEventListener('input', function (evt) {
var val = +evt.target.value
pivot[name] = val
console.log(`Setting pivot to ${pivot.x}, ${pivot.y}, ${pivot.z}`)
comp.setPivot(pivot.x, pivot.y, pivot.z)
updatePivotUI()
})
}

// Set up listener for clicking on an atom: set pivot point
stage.signals.clicked.add(function (pickingProxy) {
if (pickingProxy && pickingProxy.atom) {
const atom = pickingProxy.atom
const center = comp.getCenterUntransformed()
console.log(`Picked atom ${atom.index}; setting pivot to ${atom.x}, ${atom.y}, ${atom.z} - ctr`)
for (const name of ['x', 'y', 'z']) { pivot[name] = atom[name] - center[name] }
updatePivotUI()
comp.setPivot(atom.x - center.x, atom.y - center.y, atom.z - center.z)
}
})

// When the selection changes, we don't want the molecule to move
// around. So compute a pre-transform that will take the new matrix,
// based on the new center point, back to the old matrix. At this
// point, it's important that the matrix hasn't yet been updated to
// reflect the new selection's center point.
function onSelectionChanged (sele) {
console.log(`pivot example: selection changed to ${sele}`)

const oldMatrix = comp.matrix.clone()
console.log(`Old matrix:\n${matrix4ToString(oldMatrix)}`)

comp.updateMatrix(true) // get matrix w/ new selection-center (silently, no signals)
console.log(`New matrix:\n${matrix4ToString(comp.matrix)}`)

// Update pre-transform to make final result same as m0 (old matrix)
// T' = m0 * m1^-1 * T
tmpMat4.getInverse(comp.matrix)
comp.transform.premultiply(tmpMat4).premultiply(oldMatrix)
console.log(`Transform:\n${matrix4ToString(comp.transform)}`)
comp.updateMatrix()
}

stage.loadFile('data://1blu.pdb').then(function (o) {
comp = o
o.addRepresentation('cartoon')
o.addRepresentation('ball+stick')
o.autoView()
o.selection.signals.stringChanged.add(onSelectionChanged, o, 1) // use higher priority to run before matrix update
})
59 changes: 49 additions & 10 deletions src/component/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import Stage from '../stage/stage'
import Viewer from '../viewer/viewer'

const _m = new Matrix4()
const _v = new Vector3()
// const _v = new Vector3()

export const ComponentDefaultParameters = {
name: '',
Expand Down Expand Up @@ -69,6 +69,7 @@ abstract class Component {
quaternion = new Quaternion()
scale = new Vector3(1, 1, 1)
transform = new Matrix4()
pivot = new Vector3() // point to rotate/scale around (relative to center)

controls: ComponentControls

Expand Down Expand Up @@ -116,7 +117,7 @@ abstract class Component {
* (for global rotation use setTransform)
*
* @example
* // rotate by 2 degree radians on x axis
* // rotate by 2 radians on x axis
* component.setRotation( [ 2, 0, 0 ] );
*
* @param {Quaternion|Euler|Array} r - the rotation
Expand Down Expand Up @@ -157,6 +158,28 @@ abstract class Component {
return this
}

/**
* Set pivot point
*
* @example
* // pivot around a certain atom (assumes component is a structureComponent)
* const atom = comp.structure.getAtomProxy(atomIndex)
* const center = comp.getCenterUntransformed()
* const pos = atom.positionToVector3(new Vector3()).sub(center)
* comp.setPivot(pos.x, pos.y, pos.z)
*
* @param {number} px/py/pz - the pivot point, relative to center
* @return {Component} this object
*
* @see example at `examples/scripts/test/pivot.js`
*/
setPivot (px: number, py: number, pz: number) {
this.pivot.set(px, py, pz)
this.updateMatrix()

return this
}

/**
* Set general transform. Is applied before and in addition
* to the position, rotation and scale transformations
Expand All @@ -174,9 +197,22 @@ abstract class Component {
return this
}

updateMatrix () {
const c = this.getCenterUntransformed(_v)
this.matrix.makeTranslation(-c.x, -c.y, -c.z)
/**
* Update the component's transform matrix.
*
* The overall transform is:
* `T = transform * (position + center + pivot) * scale * rotate * -(center + pivot)`
* Note that the transform order is TSR rather than the more usual TRS.
* Transforms are applied relative to the component's center + specified pivot point.
*
* Center is defined by each subclass; for example, structure-component
* can optionally center on the current selection, or the whole structure's center.
*/
updateMatrix (silent?: boolean) {
const c = this.getCenterUntransformed()
this.matrix.makeTranslation(-c.x - this.pivot.x,
-c.y - this.pivot.y,
-c.z - this.pivot.z)

_m.makeRotationFromQuaternion(this.quaternion)
this.matrix.premultiply(_m)
Expand All @@ -185,7 +221,9 @@ abstract class Component {
this.matrix.premultiply(_m)

const p = this.position
_m.makeTranslation(p.x + c.x, p.y + c.y, p.z + c.z)
_m.makeTranslation(p.x + c.x + this.pivot.x,
p.y + c.y + this.pivot.y,
p.z + c.z + this.pivot.z)
this.matrix.premultiply(_m)

this.matrix.premultiply(this.transform)
Expand All @@ -194,11 +232,12 @@ abstract class Component {

this.stage.viewer.updateBoundingBox()

this.signals.matrixChanged.dispatch(this.matrix)
if (!silent)
this.signals.matrixChanged.dispatch(this.matrix)
}

/**
* Propogates our matrix to each representation
* Propagates our matrix to each representation
*/
updateRepresentationMatrices () {
this.reprList.forEach(repr => {
Expand Down Expand Up @@ -331,8 +370,8 @@ abstract class Component {
this.removeAllAnnotations()
this.removeAllRepresentations()

delete this.annotationList
delete this.reprList
this.annotationList = []
this.reprList = []

this.signals.disposed.dispatch()
}
Expand Down
24 changes: 20 additions & 4 deletions src/component/structure-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export interface StructureComponentSignals extends ComponentSignals {
defaultAssemblyChanged: Signal // on default assembly change
}

export type CenterMode = 'selection' | 'component'

/**
* Component wrapping a {@link Structure} object
*
Expand Down Expand Up @@ -121,6 +123,11 @@ class StructureComponent extends Component {

measureRepresentations: RepresentationCollection

// StructureComponent rotates and scales around the selection's
// center when centerMode is "selection", otherwise around the whole
// structure's center.
centerMode: CenterMode = 'selection'

constructor (stage: Stage, readonly structure: Structure, params: Partial<StructureComponentParameters> = {}) {
super(stage, structure, Object.assign({ name: structure.name }, params))

Expand Down Expand Up @@ -201,6 +208,7 @@ class StructureComponent extends Component {

this.rebuildRepresentations()
this.rebuildTrajectories()
this.updateMatrix()
fredludlow marked this conversation as resolved.
Show resolved Hide resolved
})
}

Expand Down Expand Up @@ -261,8 +269,8 @@ class StructureComponent extends Component {
}

/**
* Overrides {@link Component.updateRepresentationMatrices}
* to also update matrix for measureRepresentations
* Overrides {@link Component.updateRepresentationMatrices}
* to also update matrix for measureRepresentations
*/
updateRepresentationMatrices () {
super.updateRepresentationMatrices()
Expand Down Expand Up @@ -354,8 +362,16 @@ class StructureComponent extends Component {
return bb
}

getCenterUntransformed (sele: string): Vector3 {
if (sele && typeof sele === 'string') {
setCenterMode(mode: CenterMode) {
this.centerMode = mode
this.updateMatrix()
}

// Structure-component center can be based on the selection or the whole structure.
// Caller can override use of selection-center mode by passing useCenterMode = false.
getCenterUntransformed (useCenterMode: boolean = true): Vector3 {
fredludlow marked this conversation as resolved.
Show resolved Hide resolved
const sele = this.selection.string
if (useCenterMode && this.centerMode === 'selection' && sele && typeof sele === 'string') {
return this.structure.atomCenter(new Selection(sele))
} else {
return this.structure.center
Expand Down