super-hands
Version:
All-in-one natural hand controller interaction component for A-Frame.
142 lines (138 loc) • 5.16 kB
JavaScript
/* global AFRAME, THREE */
const inherit = AFRAME.utils.extendDeep
const physicsCore = require('./prototypes/physics-grab-proto.js')
const buttonsCore = require('./prototypes/buttons-proto.js')
// new object with all core modules
const base = inherit({}, physicsCore, buttonsCore)
AFRAME.registerComponent('grabbable', inherit(base, {
schema: {
maxGrabbers: { type: 'int', default: NaN },
invert: { default: false },
suppressY: { default: false }
},
init: function () {
this.GRABBED_STATE = 'grabbed'
this.GRAB_EVENT = 'grab-start'
this.UNGRAB_EVENT = 'grab-end'
this.grabbed = false
this.grabbers = []
this.constraints = new Map()
this.deltaPositionIsValid = false
this.grabDistance = undefined
this.grabDirection = { x: 0, y: 0, z: -1 }
this.grabOffset = { x: 0, y: 0, z: 0 }
// persistent object speeds up repeat setAttribute calls
this.destPosition = { x: 0, y: 0, z: 0 }
this.deltaPosition = new THREE.Vector3()
this.targetPosition = new THREE.Vector3()
this.physicsInit()
this.el.addEventListener(this.GRAB_EVENT, e => this.start(e))
this.el.addEventListener(this.UNGRAB_EVENT, e => this.end(e))
this.el.addEventListener('mouseout', e => this.lostGrabber(e))
},
update: function () {
this.physicsUpdate()
this.xFactor = (this.data.invert) ? -1 : 1
this.zFactor = (this.data.invert) ? -1 : 1
this.yFactor = ((this.data.invert) ? -1 : 1) * !this.data.suppressY
},
tick: (function () {
const q = new THREE.Quaternion()
const v = new THREE.Vector3()
return function () {
let entityPosition
if (this.grabber) {
// reflect on z-axis to point in same direction as the laser
this.targetPosition.copy(this.grabDirection)
this.targetPosition
.applyQuaternion(this.grabber.object3D.getWorldQuaternion(q))
.setLength(this.grabDistance)
.add(this.grabber.object3D.getWorldPosition(v))
.add(this.grabOffset)
if (this.deltaPositionIsValid) {
// relative position changes work better with nested entities
this.deltaPosition.sub(this.targetPosition)
entityPosition = this.el.getAttribute('position')
this.destPosition.x =
entityPosition.x - this.deltaPosition.x * this.xFactor
this.destPosition.y =
entityPosition.y - this.deltaPosition.y * this.yFactor
this.destPosition.z =
entityPosition.z - this.deltaPosition.z * this.zFactor
this.el.setAttribute('position', this.destPosition)
} else {
this.deltaPositionIsValid = true
}
this.deltaPosition.copy(this.targetPosition)
}
}
})(),
remove: function () {
this.el.removeEventListener(this.GRAB_EVENT, this.start)
this.el.removeEventListener(this.UNGRAB_EVENT, this.end)
this.physicsRemove()
},
start: function (evt) {
if (evt.defaultPrevented || !this.startButtonOk(evt)) {
return
}
// room for more grabbers?
const grabAvailable = !Number.isFinite(this.data.maxGrabbers) ||
this.grabbers.length < this.data.maxGrabbers
if (this.grabbers.indexOf(evt.detail.hand) === -1 && grabAvailable) {
if (!evt.detail.hand.object3D) {
console.warn('grabbable entities must have an object3D')
return
}
this.grabbers.push(evt.detail.hand)
// initiate physics if available, otherwise manual
if (!this.physicsStart(evt) && !this.grabber) {
this.grabber = evt.detail.hand
this.resetGrabber()
}
// notify super-hands that the gesture was accepted
if (evt.preventDefault) { evt.preventDefault() }
this.grabbed = true
this.el.addState(this.GRABBED_STATE)
}
},
end: function (evt) {
const handIndex = this.grabbers.indexOf(evt.detail.hand)
if (evt.defaultPrevented || !this.endButtonOk(evt)) { return }
if (handIndex !== -1) {
this.grabbers.splice(handIndex, 1)
this.grabber = this.grabbers[0]
}
this.physicsEnd(evt)
if (!this.resetGrabber()) {
this.grabbed = false
this.el.removeState(this.GRABBED_STATE)
}
if (evt.preventDefault) { evt.preventDefault() }
},
resetGrabber: (function () {
const objPos = new THREE.Vector3()
const grabPos = new THREE.Vector3()
return function () {
if (!this.grabber) {
return false
}
const raycaster = this.grabber.getAttribute('raycaster')
this.deltaPositionIsValid = false
this.grabDistance = this.el.object3D.getWorldPosition(objPos).distanceTo(this.grabber.object3D.getWorldPosition(grabPos))
if (raycaster) {
this.grabDirection = raycaster.direction
this.grabOffset = raycaster.origin
}
return true
}
})(),
lostGrabber: function (evt) {
const i = this.grabbers.indexOf(evt.relatedTarget)
// if a queued, non-physics grabber leaves the collision zone, forget it
if (i !== -1 && evt.relatedTarget !== this.grabber &&
!this.physicsIsConstrained(evt.relatedTarget)) {
this.grabbers.splice(i, 1)
}
}
}))