/* global AFRAME, THREE */ const inherit = AFRAME.utils.extendDeep; const physicsCore = require("super-hands/reaction_components/prototypes/physics-grab-proto.js"); const buttonsCore = require("super-hands/reaction_components/prototypes/buttons-proto.js"); // new object with all core modules const base = inherit({}, physicsCore, buttonsCore); AFRAME.registerComponent( "grabbable-toggle", inherit(base, { schema: { maxGrabbers: { type: "int", default: NaN }, invert: { default: false }, suppressY: { default: false }, primaryReleaseEvents: { default: ["primary_hand_release"] }, secondaryReleaseEvents: { default: ["secondary_hand_release"] } }, 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)); this.toggle = false; this.lastGrabber = null; }, 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? let grabAvailable = !Number.isFinite(this.data.maxGrabbers) || this.grabbers.length < this.data.maxGrabbers; if (Number.isFinite(this.data.maxGrabbers) && !grabAvailable && this.grabbed) { this.grabbers[0].components["super-hands"].onGrabEndButton(); grabAvailable = true; } 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; } const type = evt.detail && evt.detail.buttonEvent ? evt.detail.buttonEvent.type : null; if (this.toggle && this.lastGrabber !== this.grabbers[0]) { this.toggle = false; this.lastGrabber = null; } if (handIndex !== -1) { this.grabber = this.grabbers[0]; } if ((this.isPrimaryRelease(type) && !this.toggle) || this.isSecondaryRelease(type)) { this.toggle = true; this.lastGrabber = this.grabbers[0]; return; } else if (this.toggle && this.isPrimaryRelease(type)) { this.toggle = false; this.lastGrabber = null; } 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: (() => { 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); } }, isPrimaryRelease(type) { return this.data.primaryReleaseEvents.indexOf(type) !== -1; }, isSecondaryRelease(type) { return this.data.secondaryReleaseEvents.indexOf(type) !== -1; } }) );