diff --git a/examples/scripts/test/pivot.js b/examples/scripts/test/pivot.js
new file mode 100644
index 000000000..bce1ee16d
--- /dev/null
+++ b/examples/scripts/test/pivot.js
@@ -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 = `
`
+
+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
+})
diff --git a/src/component/component.ts b/src/component/component.ts
index 5b0f94863..8a34d3aad 100644
--- a/src/component/component.ts
+++ b/src/component/component.ts
@@ -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: '',
@@ -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
@@ -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
@@ -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
@@ -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)
@@ -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)
@@ -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 => {
@@ -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()
}
diff --git a/src/component/structure-component.ts b/src/component/structure-component.ts
index b15d6ac29..b30a04c67 100644
--- a/src/component/structure-component.ts
+++ b/src/component/structure-component.ts
@@ -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
*
@@ -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 = {}) {
super(stage, structure, Object.assign({ name: structure.name }, params))
@@ -201,6 +208,11 @@ class StructureComponent extends Component {
this.rebuildRepresentations()
this.rebuildTrajectories()
+ if (this.centerMode === 'selection')
+ // Selection's center point has changed, and in center mode we use
+ // the selection center in the matrix (via getCenterUntransformed),
+ // so we need to recalculate the matrix.
+ this.updateMatrix()
})
}
@@ -261,8 +273,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()
@@ -354,11 +366,29 @@ 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 (sele?: string, useCenterMode: boolean = true): Vector3 {
+ if (!useCenterMode)
+ return this.structure.center
+
+ if (sele !== undefined && typeof sele === 'string') {
+ // selection passed in explicitly: ignore current `centerMode`
+ // and use the selection no matter what.
return this.structure.atomCenter(new Selection(sele))
} else {
- return this.structure.center
+ // No explicitly passed selection; use current centerMode
+ sele = this.selection.string
+ if (this.centerMode === 'selection' && sele && typeof sele === 'string') {
+ return this.structure.atomCenter(new Selection(sele))
+ } else {
+ return this.structure.center
+ }
}
}
diff --git a/src/controls/trackball-controls.ts b/src/controls/trackball-controls.ts
index 26f459d77..306e9a719 100644
--- a/src/controls/trackball-controls.ts
+++ b/src/controls/trackball-controls.ts
@@ -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()
@@ -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)
@@ -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
@@ -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) // may contain non-unit scale
+ tmpRotateMatrix.premultiply(tmpRotateMatrix2)
tmpRotateMatrix.getInverse(tmpRotateMatrix)
tmpRotateMatrix.premultiply(tmpRotateCameraMatrix)