super-hands
Version:
All-in-one natural hand controller interaction component for A-Frame.
1,278 lines (1,259 loc) • 43.1 kB
JavaScript
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
/* global AFRAME */
if (typeof AFRAME === 'undefined') {
throw new Error('Component attempted to register before AFRAME was available.');
}
require('./systems/super-hands-system.js');
require('./reaction_components/hoverable.js');
require('./reaction_components/grabbable.js');
require('./reaction_components/stretchable.js');
require('./reaction_components/drag-droppable.js');
require('./reaction_components/draggable.js');
require('./reaction_components/droppable.js');
require('./reaction_components/clickable.js');
/**
* Super Hands component for A-Frame.
*/
AFRAME.registerComponent('super-hands', {
schema: {
colliderEvent: {
default: 'hit'
},
colliderEventProperty: {
default: 'el'
},
colliderEndEvent: {
default: 'hitend'
},
colliderEndEventProperty: {
default: 'el'
},
grabStartButtons: {
default: ['gripdown', 'trackpaddown', 'triggerdown', 'gripclose', 'abuttondown', 'bbuttondown', 'xbuttondown', 'ybuttondown', 'pointup', 'thumbup', 'pointingstart', 'pistolstart', 'thumbstickdown', 'mousedown', 'touchstart']
},
grabEndButtons: {
default: ['gripup', 'trackpadup', 'triggerup', 'gripopen', 'abuttonup', 'bbuttonup', 'xbuttonup', 'ybuttonup', 'pointdown', 'thumbdown', 'pointingend', 'pistolend', 'thumbstickup', 'mouseup', 'touchend']
},
stretchStartButtons: {
default: ['gripdown', 'trackpaddown', 'triggerdown', 'gripclose', 'abuttondown', 'bbuttondown', 'xbuttondown', 'ybuttondown', 'pointup', 'thumbup', 'pointingstart', 'pistolstart', 'thumbstickdown', 'mousedown', 'touchstart']
},
stretchEndButtons: {
default: ['gripup', 'trackpadup', 'triggerup', 'gripopen', 'abuttonup', 'bbuttonup', 'xbuttonup', 'ybuttonup', 'pointdown', 'thumbdown', 'pointingend', 'pistolend', 'thumbstickup', 'mouseup', 'touchend']
},
dragDropStartButtons: {
default: ['gripdown', 'trackpaddown', 'triggerdown', 'gripclose', 'abuttondown', 'bbuttondown', 'xbuttondown', 'ybuttondown', 'pointup', 'thumbup', 'pointingstart', 'pistolstart', 'thumbstickdown', 'mousedown', 'touchstart']
},
dragDropEndButtons: {
default: ['gripup', 'trackpadup', 'triggerup', 'gripopen', 'abuttonup', 'bbuttonup', 'xbuttonup', 'ybuttonup', 'pointdown', 'thumbdown', 'pointingend', 'pistolend', 'thumbstickup', 'mouseup', 'touchend']
},
interval: {
default: 0
}
},
/**
* Set if component needs multiple instancing.
*/
multiple: false,
/**
* Called once when component is attached. Generally for initial setup.
*/
init: function () {
// constants
this.HOVER_EVENT = 'hover-start';
this.UNHOVER_EVENT = 'hover-end';
this.GRAB_EVENT = 'grab-start';
this.UNGRAB_EVENT = 'grab-end';
this.STRETCH_EVENT = 'stretch-start';
this.UNSTRETCH_EVENT = 'stretch-end';
this.DRAG_EVENT = 'drag-start';
this.UNDRAG_EVENT = 'drag-end';
this.DRAGOVER_EVENT = 'dragover-start';
this.UNDRAGOVER_EVENT = 'dragover-end';
this.DRAGDROP_EVENT = 'drag-drop';
// links to other systems/components
this.otherSuperHand = null;
// state tracking - global event handlers (GEH)
this.gehDragged = new Set();
this.gehClicking = new Set();
// state tracking - reaction components
this.hoverEls = [];
this.hoverElsIntersections = [];
this.prevCheckTime = null;
this.state = new Map();
this.dragging = false;
this.unHover = this.unHover.bind(this);
this.unWatch = this.unWatch.bind(this);
this.onHit = this.onHit.bind(this);
this.onGrabStartButton = this.onGrabStartButton.bind(this);
this.onGrabEndButton = this.onGrabEndButton.bind(this);
this.onStretchStartButton = this.onStretchStartButton.bind(this);
this.onStretchEndButton = this.onStretchEndButton.bind(this);
this.onDragDropStartButton = this.onDragDropStartButton.bind(this);
this.onDragDropEndButton = this.onDragDropEndButton.bind(this);
this.system.registerMe(this);
},
/**
* Called when component is attached and when component data changes.
* Generally modifies the entity based on the data.
*/
update: function (oldData) {
this.unRegisterListeners(oldData);
this.registerListeners();
},
/**
* Called when a component is removed (e.g., via removeAttribute).
* Generally undoes all modifications to the entity.
*/
remove: function () {
this.system.unregisterMe(this);
this.unRegisterListeners();
this.hoverEls.length = 0;
if (this.state.get(this.HOVER_EVENT)) {
this._unHover(this.state.get(this.HOVER_EVENT));
}
this.onGrabEndButton();
this.onStretchEndButton();
this.onDragDropEndButton();
},
tick: function () {
// closer objects and objects with no distance come later in list
function sorter(a, b) {
const aDist = a.distance == null ? -1 : a.distance;
const bDist = b.distance == null ? -1 : b.distance;
if (aDist < bDist) {
return 1;
}
if (bDist < aDist) {
return -1;
}
return 0;
}
return function (time) {
const data = this.data;
const prevCheckTime = this.prevCheckTime;
if (prevCheckTime && time - prevCheckTime < data.interval) {
return;
}
this.prevCheckTime = time;
let orderChanged = false;
this.hoverElsIntersections.sort(sorter);
for (let i = 0; i < this.hoverElsIntersections.length; i++) {
if (this.hoverEls[i] !== this.hoverElsIntersections[i].object.el) {
orderChanged = true;
this.hoverEls[i] = this.hoverElsIntersections[i].object.el;
}
}
if (orderChanged) {
this.hover();
}
};
}(),
onGrabStartButton: function (evt) {
let carried = this.state.get(this.GRAB_EVENT);
this.dispatchMouseEventAll('mousedown', this.el);
this.gehClicking = new Set(this.hoverEls);
if (!carried) {
carried = this.findTarget(this.GRAB_EVENT, {
hand: this.el,
buttonEvent: evt
});
if (carried) {
this.state.set(this.GRAB_EVENT, carried);
this._unHover(carried);
}
}
},
onGrabEndButton: function (evt) {
const clickables = this.hoverEls.filter(h => this.gehClicking.has(h));
const grabbed = this.state.get(this.GRAB_EVENT);
const endEvt = {
hand: this.el,
buttonEvent: evt
};
this.dispatchMouseEventAll('mouseup', this.el);
for (let i = 0; i < clickables.length; i++) {
this.dispatchMouseEvent(clickables[i], 'click', this.el);
}
this.gehClicking.clear();
// check if grabbed entity accepts ungrab event
if (grabbed && !this.emitCancelable(grabbed, this.UNGRAB_EVENT, endEvt)) {
/* push to top of stack so a drop followed by re-grab gets the same
target */
this.promoteHoveredEl(this.state.get(this.GRAB_EVENT));
this.state.delete(this.GRAB_EVENT);
this.hover();
}
},
onStretchStartButton: function (evt) {
let stretched = this.state.get(this.STRETCH_EVENT);
if (!stretched) {
stretched = this.findTarget(this.STRETCH_EVENT, {
hand: this.el,
buttonEvent: evt
});
if (stretched) {
this.state.set(this.STRETCH_EVENT, stretched);
this._unHover(stretched);
}
}
},
onStretchEndButton: function (evt) {
const stretched = this.state.get(this.STRETCH_EVENT);
const endEvt = {
hand: this.el,
buttonEvent: evt
};
// check if end event accepted
if (stretched && !this.emitCancelable(stretched, this.UNSTRETCH_EVENT, endEvt)) {
this.promoteHoveredEl(stretched);
this.state.delete(this.STRETCH_EVENT);
this.hover();
}
},
onDragDropStartButton: function (evt) {
let dragged = this.state.get(this.DRAG_EVENT);
this.dragging = true;
if (this.hoverEls.length) {
this.gehDragged = new Set(this.hoverEls);
this.dispatchMouseEventAll('dragstart', this.el);
}
if (!dragged) {
/* prefer carried so that a drag started after a grab will work
with carried element rather than a currently intersected drop target.
fall back to queue in case a drag is initiated independent
of a grab */
if (this.state.get(this.GRAB_EVENT) && !this.emitCancelable(this.state.get(this.GRAB_EVENT), this.DRAG_EVENT, {
hand: this.el,
buttonEvent: evt
})) {
dragged = this.state.get(this.GRAB_EVENT);
} else {
dragged = this.findTarget(this.DRAG_EVENT, {
hand: this.el,
buttonEvent: evt
});
}
if (dragged) {
this.state.set(this.DRAG_EVENT, dragged);
this._unHover(dragged);
}
}
},
onDragDropEndButton: function (evt) {
const carried = this.state.get(this.DRAG_EVENT);
this.dragging = false; // keep _unHover() from activating another droptarget
this.gehDragged.forEach(carried => {
this.dispatchMouseEvent(carried, 'dragend', this.el);
// fire event both ways for all intersected targets
this.dispatchMouseEventAll('drop', carried, true, true);
this.dispatchMouseEventAll('dragleave', carried, true, true);
});
this.gehDragged.clear();
if (carried) {
const ddEvt = {
hand: this.el,
dropped: carried,
on: null,
buttonEvent: evt
};
const endEvt = {
hand: this.el,
buttonEvent: evt
};
const dropTarget = this.findTarget(this.DRAGDROP_EVENT, ddEvt, true);
if (dropTarget) {
ddEvt.on = dropTarget;
this.emitCancelable(carried, this.DRAGDROP_EVENT, ddEvt);
this._unHover(dropTarget);
}
// check if end event accepted
if (!this.emitCancelable(carried, this.UNDRAG_EVENT, endEvt)) {
this.promoteHoveredEl(carried);
this.state.delete(this.DRAG_EVENT);
this.hover();
}
}
},
processHitEl: function (hitEl, intersection) {
const dist = intersection && intersection.distance;
const sects = this.hoverElsIntersections;
const hoverEls = this.hoverEls;
const hitElIndex = this.hoverEls.indexOf(hitEl);
let hoverNeedsUpdate = false;
if (dist && intersection.instanceId !== undefined) hitEl.object3D.userData = {
instanceId: intersection.instanceId
};
if (hitElIndex === -1) {
hoverNeedsUpdate = true;
// insert in order of distance when available
if (dist != null) {
let i = 0;
while (i < sects.length && dist < sects[i].distance) {
i++;
}
hoverEls.splice(i, 0, hitEl);
sects.splice(i, 0, intersection);
} else {
hoverEls.push(hitEl);
sects.push({
object: {
el: hitEl
}
});
}
this.dispatchMouseEvent(hitEl, 'mouseover', this.el);
if (this.dragging && this.gehDragged.size) {
// events on targets and on dragged
this.gehDragged.forEach(dragged => {
this.dispatchMouseEventAll('dragenter', dragged, true, true);
});
}
}
return hoverNeedsUpdate;
},
onHit: function (evt) {
const hitEl = evt.detail[this.data.colliderEventProperty];
let hoverNeedsUpdate = 0;
if (!hitEl) {
return;
}
if (Array.isArray(hitEl)) {
for (let i = 0, sect; i < hitEl.length; i++) {
sect = evt.detail.intersections && evt.detail.intersections[i];
hoverNeedsUpdate += this.processHitEl(hitEl[i], sect);
}
} else {
hoverNeedsUpdate += this.processHitEl(hitEl, null);
}
if (hoverNeedsUpdate) {
this.hover();
}
},
/* search collided entities for target to hover/dragover */
hover: function () {
let hvrevt, hoverEl;
// end previous hover
if (this.state.has(this.HOVER_EVENT)) {
this._unHover(this.state.get(this.HOVER_EVENT), true);
}
if (this.state.has(this.DRAGOVER_EVENT)) {
this._unHover(this.state.get(this.DRAGOVER_EVENT), true);
}
if (this.dragging && this.state.get(this.DRAG_EVENT)) {
hvrevt = {
hand: this.el,
hovered: hoverEl,
carried: this.state.get(this.DRAG_EVENT)
};
hoverEl = this.findTarget(this.DRAGOVER_EVENT, hvrevt, true);
if (hoverEl) {
this.emitCancelable(this.state.get(this.DRAG_EVENT), this.DRAGOVER_EVENT, hvrevt);
this.state.set(this.DRAGOVER_EVENT, hoverEl);
}
}
// fallback to hover if not dragging or dragover wasn't successful
if (!this.state.has(this.DRAGOVER_EVENT)) {
hoverEl = this.findTarget(this.HOVER_EVENT, {
hand: this.el
}, true);
if (hoverEl) {
this.state.set(this.HOVER_EVENT, hoverEl);
}
}
},
/* called when controller moves out of collision range of entity */
unHover: function (evt) {
const clearedEls = evt.detail[this.data.colliderEndEventProperty];
if (clearedEls) {
if (Array.isArray(clearedEls)) {
clearedEls.forEach(el => this._unHover(el));
} else {
this._unHover(clearedEls);
}
}
},
/* inner unHover steps needed regardless of cause of unHover */
_unHover: function (el, skipNextHover) {
let unHovered = false;
let evt;
if (el === this.state.get(this.DRAGOVER_EVENT)) {
this.state.delete(this.DRAGOVER_EVENT);
unHovered = true;
evt = {
hand: this.el,
hovered: el,
carried: this.state.get(this.DRAG_EVENT)
};
this.emitCancelable(el, this.UNDRAGOVER_EVENT, evt);
if (this.state.has(this.DRAG_EVENT)) {
this.emitCancelable(this.state.get(this.DRAG_EVENT), this.UNDRAGOVER_EVENT, evt);
}
}
if (el === this.state.get(this.HOVER_EVENT)) {
this.state.delete(this.HOVER_EVENT);
unHovered = true;
this.emitCancelable(el, this.UNHOVER_EVENT, {
hand: this.el
});
}
// activate next target, if present
if (unHovered && !skipNextHover) {
this.hover();
}
},
unWatch: function (evt) {
const clearedEls = evt.detail[this.data.colliderEndEventProperty];
if (clearedEls) {
if (Array.isArray(clearedEls)) {
clearedEls.forEach(el => this._unWatch(el));
} else {
// deprecation path: sphere-collider
this._unWatch(clearedEls);
}
}
},
_unWatch: function (target) {
const hoverIndex = this.hoverEls.indexOf(target);
if (hoverIndex !== -1) {
this.hoverEls.splice(hoverIndex, 1);
this.hoverElsIntersections.splice(hoverIndex, 1);
}
this.gehDragged.forEach(dragged => {
this.dispatchMouseEvent(target, 'dragleave', dragged);
this.dispatchMouseEvent(dragged, 'dragleave', target);
});
this.dispatchMouseEvent(target, 'mouseout', this.el);
},
registerListeners: function () {
this.el.addEventListener(this.data.colliderEvent, this.onHit);
this.el.addEventListener(this.data.colliderEndEvent, this.unWatch);
this.el.addEventListener(this.data.colliderEndEvent, this.unHover);
// binding order to keep grabEnd from triggering dragover
// again before dragDropEnd can delete its carried state
this.data.grabStartButtons.forEach(b => {
this.el.addEventListener(b, this.onGrabStartButton);
});
this.data.stretchStartButtons.forEach(b => {
this.el.addEventListener(b, this.onStretchStartButton);
});
this.data.dragDropStartButtons.forEach(b => {
this.el.addEventListener(b, this.onDragDropStartButton);
});
this.data.dragDropEndButtons.forEach(b => {
this.el.addEventListener(b, this.onDragDropEndButton);
});
this.data.stretchEndButtons.forEach(b => {
this.el.addEventListener(b, this.onStretchEndButton);
});
this.data.grabEndButtons.forEach(b => {
this.el.addEventListener(b, this.onGrabEndButton);
});
},
unRegisterListeners: function (data) {
data = data || this.data;
if (Object.keys(data).length === 0) {
// Empty object passed on initalization
return;
}
this.el.removeEventListener(data.colliderEvent, this.onHit);
this.el.removeEventListener(data.colliderEndEvent, this.unHover);
this.el.removeEventListener(data.colliderEndEvent, this.unWatch);
data.grabStartButtons.forEach(b => {
this.el.removeEventListener(b, this.onGrabStartButton);
});
data.grabEndButtons.forEach(b => {
this.el.removeEventListener(b, this.onGrabEndButton);
});
data.stretchStartButtons.forEach(b => {
this.el.removeEventListener(b, this.onStretchStartButton);
});
data.stretchEndButtons.forEach(b => {
this.el.removeEventListener(b, this.onStretchEndButton);
});
data.dragDropStartButtons.forEach(b => {
this.el.removeEventListener(b, this.onDragDropStartButton);
});
data.dragDropEndButtons.forEach(b => {
this.el.removeEventListener(b, this.onDragDropEndButton);
});
},
emitCancelable: function (target, name, detail) {
detail = detail || {};
const data = {
bubbles: true,
cancelable: true,
detail
};
data.detail.target = data.detail.target || target;
const evt = new window.CustomEvent(name, data);
return target.dispatchEvent(evt);
},
dispatchMouseEvent: function (target, name, relatedTarget) {
const mEvt = new window.MouseEvent(name, {
relatedTarget
});
target.dispatchEvent(mEvt);
},
dispatchMouseEventAll: function (name, relatedTarget, filterUsed, alsoReverse) {
let els = this.hoverEls;
if (filterUsed) {
els = els.filter(el => el !== this.state.get(this.GRAB_EVENT) && el !== this.state.get(this.DRAG_EVENT) && el !== this.state.get(this.STRETCH_EVENT) && !this.gehDragged.has(el));
}
if (alsoReverse) {
for (let i = 0; i < els.length; i++) {
this.dispatchMouseEvent(els[i], name, relatedTarget);
this.dispatchMouseEvent(relatedTarget, name, els[i]);
}
} else {
for (let i = 0; i < els.length; i++) {
this.dispatchMouseEvent(els[i], name, relatedTarget);
}
}
},
findTarget: function (evType, detail, filterUsed) {
let elIndex;
let eligibleEls = this.hoverEls;
if (filterUsed) {
eligibleEls = eligibleEls.filter(el => el !== this.state.get(this.GRAB_EVENT) && el !== this.state.get(this.DRAG_EVENT) && el !== this.state.get(this.STRETCH_EVENT));
}
for (elIndex = eligibleEls.length - 1; elIndex >= 0; elIndex--) {
if (!this.emitCancelable(eligibleEls[elIndex], evType, detail)) {
return eligibleEls[elIndex];
}
}
return null;
},
// Helper to ensure dropping and regrabbing finds the same target for
// for order-sorted hoverEls (grabbing; no-op for distance-sorted (pointing)
promoteHoveredEl: function (el) {
const hoverIndex = this.hoverEls.indexOf(el);
if (hoverIndex !== -1 && this.hoverElsIntersections[hoverIndex].distance == null) {
this.hoverEls.splice(hoverIndex, 1);
const sect = this.hoverElsIntersections.splice(hoverIndex, 1);
this.hoverEls.push(el);
this.hoverElsIntersections.push(sect[0]);
}
}
});
},{"./reaction_components/clickable.js":2,"./reaction_components/drag-droppable.js":3,"./reaction_components/draggable.js":4,"./reaction_components/droppable.js":5,"./reaction_components/grabbable.js":6,"./reaction_components/hoverable.js":7,"./reaction_components/stretchable.js":10,"./systems/super-hands-system.js":11}],2:[function(require,module,exports){
/* global AFRAME */
const buttonCore = require('./prototypes/buttons-proto.js');
AFRAME.registerComponent('clickable', AFRAME.utils.extendDeep({}, buttonCore, {
schema: {
onclick: {
type: 'string'
}
},
init: function () {
this.CLICKED_STATE = 'clicked';
this.CLICK_EVENT = 'grab-start';
this.UNCLICK_EVENT = 'grab-end';
this.clickers = [];
this.start = this.start.bind(this);
this.end = this.end.bind(this);
this.el.addEventListener(this.CLICK_EVENT, this.start);
this.el.addEventListener(this.UNCLICK_EVENT, this.end);
},
remove: function () {
this.el.removeEventListener(this.CLICK_EVENT, this.start);
this.el.removeEventListener(this.UNCLICK_EVENT, this.end);
},
start: function (evt) {
if (evt.defaultPrevented || !this.startButtonOk(evt)) {
return;
}
this.el.addState(this.CLICKED_STATE);
if (this.clickers.indexOf(evt.detail.hand) === -1) {
this.clickers.push(evt.detail.hand);
if (evt.preventDefault) {
evt.preventDefault();
}
}
},
end: function (evt) {
const handIndex = this.clickers.indexOf(evt.detail.hand);
if (evt.defaultPrevented || !this.endButtonOk(evt)) {
return;
}
if (handIndex !== -1) {
this.clickers.splice(handIndex, 1);
}
if (this.clickers.length < 1) {
this.el.removeState(this.CLICKED_STATE);
}
if (evt.preventDefault) {
evt.preventDefault();
}
}
}));
},{"./prototypes/buttons-proto.js":8}],3:[function(require,module,exports){
/* global AFRAME */
const inherit = AFRAME.utils.extendDeep;
const buttonCore = require('./prototypes/buttons-proto.js');
AFRAME.registerComponent('drag-droppable', inherit({}, buttonCore, {
init: function () {
console.warn('Warning: drag-droppable is deprecated. Use draggable and droppable components instead');
this.HOVERED_STATE = 'dragover';
this.DRAGGED_STATE = 'dragged';
this.HOVER_EVENT = 'dragover-start';
this.UNHOVER_EVENT = 'dragover-end';
this.DRAG_EVENT = 'drag-start';
this.UNDRAG_EVENT = 'drag-end';
this.DRAGDROP_EVENT = 'drag-drop';
this.hoverStart = this.hoverStart.bind(this);
this.dragStart = this.dragStart.bind(this);
this.hoverEnd = this.hoverEnd.bind(this);
this.dragEnd = this.dragEnd.bind(this);
this.dragDrop = this.dragDrop.bind(this);
this.el.addEventListener(this.HOVER_EVENT, this.hoverStart);
this.el.addEventListener(this.DRAG_EVENT, this.dragStart);
this.el.addEventListener(this.UNHOVER_EVENT, this.hoverEnd);
this.el.addEventListener(this.UNDRAG_EVENT, this.dragEnd);
this.el.addEventListener(this.DRAGDROP_EVENT, this.dragDrop);
},
remove: function () {
this.el.removeEventListener(this.HOVER_EVENT, this.hoverStart);
this.el.removeEventListener(this.DRAG_EVENT, this.dragStart);
this.el.removeEventListener(this.UNHOVER_EVENT, this.hoverEnd);
this.el.removeEventListener(this.UNDRAG_EVENT, this.dragEnd);
this.el.removeEventListener(this.DRAGDROP_EVENT, this.dragDrop);
},
hoverStart: function (evt) {
this.el.addState(this.HOVERED_STATE);
if (evt.preventDefault) {
evt.preventDefault();
}
},
dragStart: function (evt) {
if (!this.startButtonOk(evt)) {
return;
}
this.el.addState(this.DRAGGED_STATE);
if (evt.preventDefault) {
evt.preventDefault();
}
},
hoverEnd: function (evt) {
this.el.removeState(this.HOVERED_STATE);
},
dragEnd: function (evt) {
if (!this.endButtonOk(evt)) {
return;
}
this.el.removeState(this.DRAGGED_STATE);
if (evt.preventDefault) {
evt.preventDefault();
}
},
dragDrop: function (evt) {
if (!this.endButtonOk(evt)) {
return;
}
if (evt.preventDefault) {
evt.preventDefault();
}
}
}));
},{"./prototypes/buttons-proto.js":8}],4:[function(require,module,exports){
/* global AFRAME */
const inherit = AFRAME.utils.extendDeep;
const buttonCore = require('./prototypes/buttons-proto.js');
AFRAME.registerComponent('draggable', inherit({}, buttonCore, {
init: function () {
this.DRAGGED_STATE = 'dragged';
this.DRAG_EVENT = 'drag-start';
this.UNDRAG_EVENT = 'drag-end';
this.dragStartBound = this.dragStart.bind(this);
this.dragEndBound = this.dragEnd.bind(this);
this.el.addEventListener(this.DRAG_EVENT, this.dragStartBound);
this.el.addEventListener(this.UNDRAG_EVENT, this.dragEndBound);
},
remove: function () {
this.el.removeEventListener(this.DRAG_EVENT, this.dragStart);
this.el.removeEventListener(this.UNDRAG_EVENT, this.dragEnd);
},
dragStart: function (evt) {
if (evt.defaultPrevented || !this.startButtonOk(evt)) {
return;
}
this.el.addState(this.DRAGGED_STATE);
if (evt.preventDefault) {
evt.preventDefault();
}
},
dragEnd: function (evt) {
if (evt.defaultPrevented || !this.endButtonOk(evt)) {
return;
}
this.el.removeState(this.DRAGGED_STATE);
if (evt.preventDefault) {
evt.preventDefault();
}
}
}));
},{"./prototypes/buttons-proto.js":8}],5:[function(require,module,exports){
/* global AFRAME */
function elementMatches(el, selector) {
if (el.matches) {
return el.matches(selector);
}
if (el.msMatchesSelector) {
return el.msMatchesSelector(selector);
}
if (el.webkitMatchesSelector) {
return el.webkitMatchesSelector(selector);
}
}
AFRAME.registerComponent('droppable', {
schema: {
accepts: {
default: ''
},
autoUpdate: {
default: true
},
acceptEvent: {
default: ''
},
rejectEvent: {
default: ''
}
},
multiple: true,
init: function () {
this.HOVERED_STATE = 'dragover';
this.HOVER_EVENT = 'dragover-start';
this.UNHOVER_EVENT = 'dragover-end';
this.DRAGDROP_EVENT = 'drag-drop';
// better for Sinon spying if original method not overwritten
this.hoverStartBound = this.hoverStart.bind(this);
this.hoverEndBound = this.hoverEnd.bind(this);
this.dragDropBound = this.dragDrop.bind(this);
this.mutateAcceptsBound = this.mutateAccepts.bind(this);
this.acceptableEntities = [];
this.observer = new window.MutationObserver(this.mutateAcceptsBound);
this.observerOpts = {
childList: true,
subtree: true
};
this.el.addEventListener(this.HOVER_EVENT, this.hoverStartBound);
this.el.addEventListener(this.UNHOVER_EVENT, this.hoverEndBound);
this.el.addEventListener(this.DRAGDROP_EVENT, this.dragDropBound);
},
update: function () {
if (this.data.accepts.length) {
this.acceptableEntities = Array.prototype.slice.call(this.el.sceneEl.querySelectorAll(this.data.accepts));
} else {
this.acceptableEntities = null;
}
if (this.data.autoUpdate && this.acceptableEntities != null) {
this.observer.observe(this.el.sceneEl, this.observerOpts);
} else {
this.observer.disconnect();
}
},
remove: function () {
this.el.removeEventListener(this.HOVER_EVENT, this.hoverStartBound);
this.el.removeEventListener(this.UNHOVER_EVENT, this.hoverEndBound);
this.el.removeEventListener(this.DRAGDROP_EVENT, this.dragDropBound);
this.observer.disconnect();
},
mutateAccepts: function (mutations) {
const query = this.data.accepts;
mutations.forEach(mutation => {
mutation.addedNodes.forEach(added => {
if (elementMatches(added, query)) {
this.acceptableEntities.push(added);
}
});
});
},
entityAcceptable: function (entity) {
const acceptableEntities = this.acceptableEntities;
if (acceptableEntities == null) {
return true;
}
for (const item of acceptableEntities) {
if (item === entity) {
return true;
}
}
return false;
},
hoverStart: function (evt) {
if (evt.defaultPrevented || !this.entityAcceptable(evt.detail.carried)) {
return;
}
this.el.addState(this.HOVERED_STATE);
if (evt.preventDefault) {
evt.preventDefault();
}
},
hoverEnd: function (evt) {
if (evt.defaultPrevented) {
return;
}
this.el.removeState(this.HOVERED_STATE);
},
dragDrop: function (evt) {
if (evt.defaultPrevented) {
return;
}
const dropped = evt.detail.dropped;
if (!this.entityAcceptable(dropped)) {
if (this.data.rejectEvent.length) {
this.el.emit(this.data.rejectEvent, {
el: dropped
});
}
return;
}
if (this.data.acceptEvent.length) {
this.el.emit(this.data.acceptEvent, {
el: dropped
});
}
if (evt.preventDefault) {
evt.preventDefault();
}
}
});
},{}],6:[function(require,module,exports){
/* 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);
}
}
}));
},{"./prototypes/buttons-proto.js":8,"./prototypes/physics-grab-proto.js":9}],7:[function(require,module,exports){
/* global AFRAME */
AFRAME.registerComponent('hoverable', {
init: function () {
this.HOVERED_STATE = 'hovered';
this.HOVER_EVENT = 'hover-start';
this.UNHOVER_EVENT = 'hover-end';
this.hoverers = [];
this.start = this.start.bind(this);
this.end = this.end.bind(this);
this.el.addEventListener(this.HOVER_EVENT, this.start);
this.el.addEventListener(this.UNHOVER_EVENT, this.end);
},
remove: function () {
this.el.removeEventListener(this.HOVER_EVENT, this.start);
this.el.removeEventListener(this.UNHOVER_EVENT, this.end);
},
start: function (evt) {
if (evt.defaultPrevented) {
return;
}
this.el.addState(this.HOVERED_STATE);
if (this.hoverers.indexOf(evt.detail.hand) === -1) {
this.hoverers.push(evt.detail.hand);
}
if (evt.preventDefault) {
evt.preventDefault();
}
},
end: function (evt) {
if (evt.defaultPrevented) {
return;
}
const handIndex = this.hoverers.indexOf(evt.detail.hand);
if (handIndex !== -1) {
this.hoverers.splice(handIndex, 1);
}
if (this.hoverers.length < 1) {
this.el.removeState(this.HOVERED_STATE);
}
}
});
},{}],8:[function(require,module,exports){
// common code used in customizing reaction components by button
module.exports = function () {
function buttonIsValid(evt, buttonList) {
return buttonList.length === 0 || buttonList.indexOf(evt.detail.buttonEvent.type) !== -1;
}
return {
schema: {
startButtons: {
default: []
},
endButtons: {
default: []
}
},
startButtonOk: function (evt) {
return buttonIsValid(evt, this.data.startButtons);
},
endButtonOk: function (evt) {
return buttonIsValid(evt, this.data.endButtons);
}
};
}();
},{}],9:[function(require,module,exports){
// base code used by grabbable for physics interactions
module.exports = {
schema: {
usePhysics: {
default: 'ifavailable'
}
},
physicsInit: function () {
this.constraints = new Map();
},
physicsUpdate: function () {
if (this.data.usePhysics === 'never' && this.constraints.size) {
this.physicsClear();
}
},
physicsRemove: function () {
this.physicsClear();
},
physicsStart: function (evt) {
// initiate physics constraint if available and not already existing
if (this.data.usePhysics !== 'never' && this.el.body && evt.detail.hand.body && !this.constraints.has(evt.detail.hand)) {
const newConId = Math.random().toString(36).substr(2, 9);
this.el.setAttribute('constraint__' + newConId, {
target: evt.detail.hand
});
this.constraints.set(evt.detail.hand, newConId);
return true;
}
// Prevent manual grab by returning true
if (this.data.usePhysics === 'only') {
return true;
}
return false;
},
physicsEnd: function (evt) {
const constraintId = this.constraints.get(evt.detail.hand);
if (constraintId) {
this.el.removeAttribute('constraint__' + constraintId);
this.constraints.delete(evt.detail.hand);
}
},
physicsClear: function () {
if (this.el.body) {
for (const c of this.constraints.values()) {
this.el.body.world.removeConstraint(c);
}
}
this.constraints.clear();
},
physicsIsConstrained: function (el) {
return this.constraints.has(el);
},
physicsIsGrabbing() {
return this.constraints.size > 0;
}
};
},{}],10:[function(require,module,exports){
/* global AFRAME, THREE */
const inherit = AFRAME.utils.extendDeep;
const buttonsCore = require('./prototypes/buttons-proto.js');
// new object with all core modules
const base = inherit({}, buttonsCore);
AFRAME.registerComponent('stretchable', inherit(base, {
schema: {
usePhysics: {
default: 'ifavailable'
},
invert: {
default: false
},
physicsUpdateRate: {
default: 100
}
},
init: function () {
this.STRETCHED_STATE = 'stretched';
this.STRETCH_EVENT = 'stretch-start';
this.UNSTRETCH_EVENT = 'stretch-end';
this.stretched = false;
this.stretchers = [];
this.scale = new THREE.Vector3();
this.handPos = new THREE.Vector3();
this.otherHandPos = new THREE.Vector3();
this.start = this.start.bind(this);
this.end = this.end.bind(this);
this.el.addEventListener(this.STRETCH_EVENT, this.start);
this.el.addEventListener(this.UNSTRETCH_EVENT, this.end);
},
update: function (oldDat) {
this.updateBodies = AFRAME.utils.throttleTick(this._updateBodies, this.data.physicsUpdateRate, this);
},
tick: function (time, timeDelta) {
if (!this.stretched) {
return;
}
this.scale.copy(this.el.getAttribute('scale'));
this.stretchers[0].object3D.getWorldPosition(this.handPos);
this.stretchers[1].object3D.getWorldPosition(this.otherHandPos);
const currentStretch = this.handPos.distanceTo(this.otherHandPos);
let deltaStretch = 1;
if (this.previousStretch !== null && currentStretch !== 0) {
deltaStretch = Math.pow(currentStretch / this.previousStretch, this.data.invert ? -1 : 1);
}
this.previousStretch = currentStretch;
if (this.previousPhysicsStretch == null) {
// establish correct baseline even if throttled function isn't called
this.previousPhysicsStretch = currentStretch;
}
this.scale.multiplyScalar(deltaStretch);
this.el.setAttribute('scale', this.scale);
// scale update for all nested physics bodies (throttled)
this.updateBodies(time, timeDelta);
},
remove: function () {
this.el.removeEventListener(this.STRETCH_EVENT, this.start);
this.el.removeEventListener(this.UNSTRETCH_EVENT, this.end);
},
start: function (evt) {
if (this.stretched || this.stretchers.includes(evt.detail.hand) || !this.startButtonOk(evt) || evt.defaultPrevented) {
return;
} // already stretched or already captured this hand or wrong button
this.stretchers.push(evt.detail.hand);
if (this.stretchers.length === 2) {
this.stretched = true;
this.previousStretch = null;
this.previousPhysicsStretch = null;
this.el.addState(this.STRETCHED_STATE);
}
if (evt.preventDefault) {
evt.preventDefault();
} // gesture accepted
},
end: function (evt) {
const stretcherIndex = this.stretchers.indexOf(evt.detail.hand);
if (evt.defaultPrevented || !this.endButtonOk(evt)) {
return;
}
if (stretcherIndex !== -1) {
this.stretchers.splice(stretcherIndex, 1);
this.stretched = false;
this.el.removeState(this.STRETCHED_STATE);
// override throttle to push last stretch to physics bodies
this._updateBodies();
}
if (evt.preventDefault) {
evt.preventDefault();
}
},
_updateBodies: function () {
if (!this.el.body || this.data.usePhysics === 'never') {
return;
}
const currentStretch = this.previousStretch; // last visible geometry stretch
let deltaStretch = 1;
if (this.previousPhysicsStretch !== null && currentStretch > 0) {
deltaStretch = Math.pow(currentStretch / this.previousPhysicsStretch, this.data.invert ? -1 : 1);
}
this.previousPhysicsStretch = currentStretch;
if (deltaStretch === 1) {
return;
}
for (const c of this.el.childNodes) {
this.stretchBody(c, deltaStretch);
}
this.stretchBody(this.el, deltaStretch);
},
stretchBody: function (el, deltaStretch) {
if (!el.body) {
return;
}
let physicsShape;
let offset;
for (let i = 0; i < el.body.shapes.length; i++) {
physicsShape = el.body.shapes[i];
if (physicsShape.halfExtents) {
physicsShape.halfExtents.scale(deltaStretch, physicsShape.halfExtents);
physicsShape.updateConvexPolyhedronRepresentation();
} else if (physicsShape.radius) {
physicsShape.radius *= deltaStretch;
physicsShape.updateBoundingSphereRadius();
} else if (!this.shapeWarned) {
console.warn('Unable to stretch physics body: unsupported shape');
this.shapeWarned = true;
}
// also move offset to match scale change
offset = el.body.shapeOffsets[i];
offset.scale(deltaStretch, offset);
}
el.body.updateBoundingRadius();
}
}));
},{"./prototypes/buttons-proto.js":8}],11:[function(require,module,exports){
/* global AFRAME */
AFRAME.registerSystem('super-hands', {
init: function () {
this.superHands = [];
},
registerMe: function (comp) {
// when second hand registers, store links
if (this.superHands.length === 1) {
this.superHands[0].otherSuperHand = comp;
comp.otherSuperHand = this.superHands[0];
}
this.superHands.push(comp);
},
unregisterMe: function (comp) {
const index = this.superHands.indexOf(comp);
if (index !== -1) {
this.superHands.splice(index, 1);
}
this.superHands.forEach(x => {
if (x.otherSuperHand === comp) {
x.otherSuperHand = null;
}
});
}
});
},{}]},{},[1]);