aframe-extras
Version:
Add-ons and examples for A-Frame VR.
585 lines (492 loc) • 18.7 kB
JavaScript
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else {
var a = factory();
for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
}
})(self, () => {
return /******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./src/misc/checkpoint.js":
/*!********************************!*\
!*** ./src/misc/checkpoint.js ***!
\********************************/
/***/ (() => {
AFRAME.registerComponent('checkpoint', {
schema: {
offset: {default: {x: 0, y: 0, z: 0}, type: 'vec3'}
},
init: function () {
this.active = false;
this.targetEl = null;
this.fire = this.fire.bind(this);
this.offset = new THREE.Vector3();
},
update: function () {
this.offset.copy(this.data.offset);
},
play: function () { this.el.addEventListener('click', this.fire); },
pause: function () { this.el.removeEventListener('click', this.fire); },
remove: function () { this.pause(); },
fire: function () {
const targetEl = this.el.sceneEl.querySelector('[checkpoint-controls]');
if (!targetEl) {
throw new Error('No `checkpoint-controls` component found.');
}
targetEl.components['checkpoint-controls'].setCheckpoint(this.el);
},
getOffset: function () {
return this.offset.copy(this.data.offset);
}
});
/***/ }),
/***/ "./src/misc/cube-env-map.js":
/*!**********************************!*\
!*** ./src/misc/cube-env-map.js ***!
\**********************************/
/***/ (() => {
/**
* @param {Array<THREE.Material>|THREE.Material} material
* @return {Array<THREE.Material>}
*/
function ensureMaterialArray (material) {
if (!material) {
return [];
} else if (Array.isArray(material)) {
return material;
} else if (material.materials) {
return material.materials;
} else {
return [material];
}
}
/**
* @param {THREE.Object3D} mesh
* @param {Array<string>} materialNames
* @param {THREE.Texture} envMap
* @param {number} reflectivity [description]
*/
function applyEnvMap (mesh, materialNames, envMap, reflectivity) {
if (!mesh) return;
materialNames = materialNames || [];
mesh.traverse((node) => {
if (!node.isMesh) return;
const meshMaterials = ensureMaterialArray(node.material);
meshMaterials.forEach((material) => {
if (material && !('envMap' in material)) return;
if (materialNames.length && materialNames.indexOf(material.name) === -1) return;
material.envMap = envMap;
material.reflectivity = reflectivity;
material.needsUpdate = true;
});
});
}
/**
* Specifies an envMap on an entity, without replacing any existing material
* properties.
*/
AFRAME.registerComponent('cube-env-map', {
multiple: true,
schema: {
path: {default: ''},
extension: {default: 'jpg', oneOf: ['jpg', 'png']},
enableBackground: {default: false},
reflectivity: {default: 1, min: 0, max: 1},
materials: {default: []}
},
init: function () {
const data = this.data;
this.texture = new THREE.CubeTextureLoader().load([
data.path + 'posx.' + data.extension, data.path + 'negx.' + data.extension,
data.path + 'posy.' + data.extension, data.path + 'negy.' + data.extension,
data.path + 'posz.' + data.extension, data.path + 'negz.' + data.extension
]);
this.texture.format = THREE.RGBAFormat;
this.object3dsetHandler = () => {
const mesh = this.el.getObject3D('mesh');
const data = this.data;
applyEnvMap(mesh, data.materials, this.texture, data.reflectivity);
};
this.object3dsetHandler();
this.el.addEventListener('object3dset', this.object3dsetHandler);
},
update: function (oldData) {
const data = this.data;
const mesh = this.el.getObject3D('mesh');
let addedMaterialNames = [];
let removedMaterialNames = [];
if (data.materials.length) {
if (oldData.materials) {
addedMaterialNames = data.materials.filter((name) => !oldData.materials.includes(name));
removedMaterialNames = oldData.materials.filter((name) => !data.materials.includes(name));
} else {
addedMaterialNames = data.materials;
}
}
if (addedMaterialNames.length) {
applyEnvMap(mesh, addedMaterialNames, this.texture, data.reflectivity);
}
if (removedMaterialNames.length) {
applyEnvMap(mesh, removedMaterialNames, null, 1);
}
if (oldData.materials && data.reflectivity !== oldData.reflectivity) {
const maintainedMaterialNames = data.materials
.filter((name) => oldData.materials.includes(name));
if (maintainedMaterialNames.length) {
applyEnvMap(mesh, maintainedMaterialNames, this.texture, data.reflectivity);
}
}
if (this.data.enableBackground && !oldData.enableBackground) {
this.setBackground(this.texture);
} else if (!this.data.enableBackground && oldData.enableBackground) {
this.setBackground(null);
}
},
remove: function () {
this.el.removeEventListener('object3dset', this.object3dsetHandler);
const mesh = this.el.getObject3D('mesh');
const data = this.data;
applyEnvMap(mesh, data.materials, null, 1);
if (data.enableBackground) this.setBackground(null);
},
setBackground: function (texture) {
this.el.sceneEl.object3D.background = texture;
}
});
/***/ }),
/***/ "./src/misc/grab.js":
/*!**************************!*\
!*** ./src/misc/grab.js ***!
\**************************/
/***/ (() => {
/* global CANNON */
/**
* Based on aframe/examples/showcase/tracked-controls.
*
* Handles events coming from the hand-controls.
* Determines if the entity is grabbed or released.
* Updates its position to move along the controller.
*/
AFRAME.registerComponent('grab', {
init: function () {
this.system = this.el.sceneEl.systems.physics;
this.GRABBED_STATE = 'grabbed';
this.grabbing = false;
this.hitEl = /** @type {AFRAME.Element} */ null;
this.physics = /** @type {AFRAME.System} */ this.el.sceneEl.systems.physics;
this.constraint = /** @type {CANNON.Constraint} */ null;
// Bind event handlers
this.onHit = this.onHit.bind(this);
this.onGripOpen = this.onGripOpen.bind(this);
this.onGripClose = this.onGripClose.bind(this);
},
play: function () {
const el = this.el;
el.addEventListener('hit', this.onHit);
el.addEventListener('gripdown', this.onGripClose);
el.addEventListener('gripup', this.onGripOpen);
el.addEventListener('trackpaddown', this.onGripClose);
el.addEventListener('trackpadup', this.onGripOpen);
el.addEventListener('triggerdown', this.onGripClose);
el.addEventListener('triggerup', this.onGripOpen);
},
pause: function () {
const el = this.el;
el.removeEventListener('hit', this.onHit);
el.removeEventListener('gripdown', this.onGripClose);
el.removeEventListener('gripup', this.onGripOpen);
el.removeEventListener('trackpaddown', this.onGripClose);
el.removeEventListener('trackpadup', this.onGripOpen);
el.removeEventListener('triggerdown', this.onGripClose);
el.removeEventListener('triggerup', this.onGripOpen);
},
onGripClose: function () {
this.grabbing = true;
},
onGripOpen: function () {
const hitEl = this.hitEl;
this.grabbing = false;
if (!hitEl) { return; }
hitEl.removeState(this.GRABBED_STATE);
this.hitEl = undefined;
this.system.removeConstraint(this.constraint);
this.constraint = null;
},
onHit: function (evt) {
const hitEl = evt.detail.el;
// If the element is already grabbed (it could be grabbed by another controller).
// If the hand is not grabbing the element does not stick.
// If we're already grabbing something you can't grab again.
if (hitEl.is(this.GRABBED_STATE) || !this.grabbing || this.hitEl) { return; }
hitEl.addState(this.GRABBED_STATE);
this.hitEl = hitEl;
this.constraint = new CANNON.LockConstraint(this.el.body, hitEl.body);
this.system.addConstraint(this.constraint);
}
});
/***/ }),
/***/ "./src/misc/normal-material.js":
/*!*************************************!*\
!*** ./src/misc/normal-material.js ***!
\*************************************/
/***/ (() => {
/**
* Recursively applies a MeshNormalMaterial to the entity, such that
* face colors are determined by their orientation. Helpful for
* debugging geometry
*/
AFRAME.registerComponent('normal-material', {
init: function () {
this.material = new THREE.MeshNormalMaterial({flatShading: true});
this.applyMaterial = this.applyMaterial.bind(this);
this.el.addEventListener('object3dset', this.applyMaterial);
this.applyMaterial();
},
remove: function () {
this.el.removeEventListener('object3dset', this.applyMaterial);
},
applyMaterial: function () {
this.el.object3D.traverse((node) => {
if (node.isMesh) node.material = this.material;
});
}
});
/***/ }),
/***/ "./src/misc/sphere-collider.js":
/*!*************************************!*\
!*** ./src/misc/sphere-collider.js ***!
\*************************************/
/***/ (() => {
/**
* Based on aframe/examples/showcase/tracked-controls.
*
* Implement bounding sphere collision detection for entities with a mesh.
* Sets the specified state on the intersected entities.
*
* @property {string} objects - Selector of the entities to test for collision.
* @property {string} state - State to set on collided entities.
*
*/
AFRAME.registerComponent('sphere-collider', {
schema: {
enabled: {default: true},
interval: {default: 80},
objects: {default: ''},
state: {default: 'collided'},
radius: {default: 0.05},
watch: {default: true}
},
init: function () {
/** @type {MutationObserver} */
this.observer = null;
/** @type {Array<Element>} Elements to watch for collisions. */
this.els = [];
/** @type {Array<Element>} Elements currently in collision state. */
this.collisions = [];
this.prevCheckTime = undefined;
this.eventDetail = {};
this.handleHit = this.handleHit.bind(this);
this.handleHitEnd = this.handleHitEnd.bind(this);
},
play: function () {
const sceneEl = this.el.sceneEl;
if (this.data.watch) {
this.observer = new MutationObserver(this.update.bind(this, null));
this.observer.observe(sceneEl, {childList: true, subtree: true});
}
},
pause: function () {
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
},
/**
* Update list of entities to test for collision.
*/
update: function () {
const data = this.data;
let objectEls;
// Push entities into list of els to intersect.
if (data.objects) {
objectEls = this.el.sceneEl.querySelectorAll(data.objects);
} else {
// If objects not defined, intersect with everything.
objectEls = this.el.sceneEl.children;
}
// Convert from NodeList to Array
this.els = Array.prototype.slice.call(objectEls);
},
tick: (function () {
const position = new THREE.Vector3(),
meshPosition = new THREE.Vector3(),
colliderScale = new THREE.Vector3(),
size = new THREE.Vector3(),
box = new THREE.Box3(),
collisions = [],
distanceMap = new Map();
return function (time) {
if (!this.data.enabled) { return; }
// Only check for intersection if interval time has passed.
const prevCheckTime = this.prevCheckTime;
if (prevCheckTime && (time - prevCheckTime < this.data.interval)) { return; }
// Update check time.
this.prevCheckTime = time;
const el = this.el,
data = this.data,
mesh = el.getObject3D('mesh');
let colliderRadius;
if (!mesh) { return; }
collisions.length = 0;
distanceMap.clear();
el.object3D.getWorldPosition(position);
el.object3D.getWorldScale(colliderScale);
colliderRadius = data.radius * scaleFactor(colliderScale);
// Update collision list.
this.els.forEach(intersect);
// Emit events and add collision states, in order of distance.
collisions
.sort((a, b) => distanceMap.get(a) > distanceMap.get(b) ? 1 : -1)
.forEach(this.handleHit);
// Remove collision state from other elements.
this.collisions
.filter((el) => !distanceMap.has(el))
.forEach(this.handleHitEnd);
// Store new collisions
copyArray(this.collisions, collisions);
// Bounding sphere collision detection
function intersect (el) {
let radius, mesh, distance, extent;
if (!el.isEntity) { return; }
mesh = el.getObject3D('mesh');
if (!mesh) { return; }
box.setFromObject(mesh).getSize(size);
extent = Math.max(size.x, size.y, size.z) / 2;
radius = Math.sqrt(2 * extent * extent);
box.getCenter(meshPosition);
if (!radius) { return; }
distance = position.distanceTo(meshPosition);
if (distance < radius + colliderRadius) {
collisions.push(el);
distanceMap.set(el, distance);
}
}
// use max of scale factors to maintain bounding sphere collision
function scaleFactor (scaleVec) {
return Math.max(scaleVec.x, scaleVec.y, scaleVec.z);
}
};
})(),
handleHit: function (targetEl) {
targetEl.emit('hit');
targetEl.addState(this.data.state);
this.eventDetail.el = targetEl;
this.el.emit('hit', this.eventDetail);
},
handleHitEnd: function (targetEl) {
targetEl.emit('hitend');
targetEl.removeState(this.data.state);
this.eventDetail.el = targetEl;
this.el.emit('hitend', this.eventDetail);
}
});
function copyArray (dest, source) {
dest.length = 0;
for (let i = 0; i < source.length; i++) { dest[i] = source[i]; }
}
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/compat get default export */
/******/ (() => {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = (module) => {
/******/ var getter = module && module.__esModule ?
/******/ () => (module['default']) :
/******/ () => (module);
/******/ __webpack_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/************************************************************************/
var __webpack_exports__ = {};
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode.
(() => {
"use strict";
/*!***************************!*\
!*** ./src/misc/index.js ***!
\***************************/
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _checkpoint_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./checkpoint.js */ "./src/misc/checkpoint.js");
/* harmony import */ var _checkpoint_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_checkpoint_js__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var _cube_env_map_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./cube-env-map.js */ "./src/misc/cube-env-map.js");
/* harmony import */ var _cube_env_map_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_cube_env_map_js__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var _grab_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./grab.js */ "./src/misc/grab.js");
/* harmony import */ var _grab_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_grab_js__WEBPACK_IMPORTED_MODULE_2__);
/* harmony import */ var _normal_material_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./normal-material.js */ "./src/misc/normal-material.js");
/* harmony import */ var _normal_material_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_normal_material_js__WEBPACK_IMPORTED_MODULE_3__);
/* harmony import */ var _sphere_collider_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./sphere-collider.js */ "./src/misc/sphere-collider.js");
/* harmony import */ var _sphere_collider_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(_sphere_collider_js__WEBPACK_IMPORTED_MODULE_4__);
})();
/******/ return __webpack_exports__;
/******/ })()
;
});
//# sourceMappingURL=aframe-extras.misc.js.map