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 2 commits
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
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
7 changes: 5 additions & 2 deletions src/controls/trackball-controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const tmpRotateXMatrix = new Matrix4()
const tmpRotateYMatrix = new Matrix4()
const tmpRotateZMatrix = new Matrix4()
const tmpRotateMatrix = new Matrix4()
const tmpRotateMatrix2 = new Matrix4()
const tmpRotateCameraMatrix = new Matrix4()
const tmpRotateVector = new Vector3()
const tmpRotateQuaternion = new Quaternion()
Expand Down Expand Up @@ -87,6 +88,7 @@ class TrackballControls {

// Adjust for component and scene rotation
tmpPanMatrix.extractRotation(this.component.transform)
// Note: use rotation _and_ scale of rotationGroup here
tmpPanMatrix.premultiply(this.viewer.rotationGroup.matrix)
tmpPanMatrix.getInverse(tmpPanMatrix)

Expand All @@ -103,7 +105,7 @@ class TrackballControls {
pan (x: number, y: number) {
this._setPanVector(x, y)

// Adjust for scene rotation
// Adjust for scene rotation and scale
tmpPanMatrix.getInverse(this.viewer.rotationGroup.matrix)

// Adjust for camera rotation
Expand Down Expand Up @@ -170,7 +172,8 @@ class TrackballControls {
this._getCameraRotation(tmpRotateCameraMatrix)

tmpRotateMatrix.extractRotation(this.component.transform)
tmpRotateMatrix.premultiply(this.viewer.rotationGroup.matrix)
tmpRotateMatrix2.extractRotation(this.viewer.rotationGroup.matrix)
fredludlow marked this conversation as resolved.
Show resolved Hide resolved
tmpRotateMatrix.premultiply(tmpRotateMatrix2)
tmpRotateMatrix.getInverse(tmpRotateMatrix)
tmpRotateMatrix.premultiply(tmpRotateCameraMatrix)

Expand Down