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 = `

Example of Setting Pivot Point

+

Adjust pivot point X with slider, then use Ctrl+Shift+Left-drag to rotate comp.
+Click on an atom to set pivot point to that atom (instead of center).
+To test center modes, set comp selection to (e.g.) "1-20" and rotate it. +
+Things to look for: in selection-center mode, changing the selection
+shouldn't move the molecule even if it's been translated, scaled, and
+rotated. In that mode, the molecule should scale and rotate about the
+selection's center. +

+

Pivot point X/Y/Z: [, , ]

+
+
+ +

Current center mode: selection

+ +
` + +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)