Click + Drag component
This commit is contained in:
commit
f446d1bb11
12 changed files with 804 additions and 0 deletions
2
.eslintignore
Normal file
2
.eslintignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
**/*.json
|
||||
/lib/
|
24
.eslintrc
Normal file
24
.eslintrc
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"extends": "airbnb",
|
||||
"rules": {
|
||||
"react/prefer-stateless-function": "off",
|
||||
"react/sort-comp": "off",
|
||||
"no-param-reassign": "off",
|
||||
"padded-blocks": "off",
|
||||
|
||||
"react/jsx-filename-extension": ["error", { "extensions": [".js", ".jsx"] }],
|
||||
"react/prefer-es6-class": ["error", "never"],
|
||||
"object-curly-spacing": ["error", "never"],
|
||||
"no-underscore-dangle": ["error", { "allowAfterThis": true }],
|
||||
"no-unused-vars": ["error", { "argsIgnorePattern": "_" }],
|
||||
"new-cap": ["error", { "capIsNewExceptions": ["React.Children"] }]
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
}
|
||||
}
|
||||
}
|
53
.gitignore
vendored
Normal file
53
.gitignore
vendored
Normal file
|
@ -0,0 +1,53 @@
|
|||
dist/
|
||||
lib/
|
||||
.sw[ponm]
|
||||
examples/build.js
|
||||
examples/node_modules/
|
||||
gh-pages
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
|
||||
# Created by https://www.gitignore.io/api/node
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
jspm_packages
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
52
.npmignore
Normal file
52
.npmignore
Normal file
|
@ -0,0 +1,52 @@
|
|||
src/
|
||||
.sw[ponm]
|
||||
examples/build.js
|
||||
examples/node_modules/
|
||||
gh-pages
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
|
||||
# Created by https://www.gitignore.io/api/node
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
jspm_packages
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
22
LICENSE
Normal file
22
LICENSE
Normal file
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Kevin Ngo
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
47
README.md
Normal file
47
README.md
Normal file
|
@ -0,0 +1,47 @@
|
|||
# aframe-click-drag-component
|
||||
|
||||
A Click & Drag component for [A-Frame](https://aframe.io).
|
||||
|
||||
Entities with the `click-drag` component can be click and dragged around the 3D
|
||||
scene. Even works whle the camera is moving!
|
||||
|
||||
_Note: entities are not positioned correctly when the camera is rotated._
|
||||
|
||||
### Installation
|
||||
|
||||
#### Browser
|
||||
|
||||
Use directly from the unpkg CDN:
|
||||
|
||||
```html
|
||||
<head>
|
||||
<script src="https://aframe.io/releases/0.3.0/aframe.min.js"></script>
|
||||
<script src="https://unpkg.com/aframe-click-drag-component"></script>
|
||||
<script>
|
||||
registerAframeClickDragComponent(window.AFRAME);
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<a-scene>
|
||||
<a-sphere click-drag position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
|
||||
<a-camera look-controls-enabled="false"></a-camera>
|
||||
</a-scene>
|
||||
</body>
|
||||
```
|
||||
|
||||
#### npm
|
||||
|
||||
Install via npm:
|
||||
|
||||
```bash
|
||||
npm install aframe-click-drag-component
|
||||
```
|
||||
|
||||
Then register and use.
|
||||
|
||||
```javascript
|
||||
import aframe from 'aframe';
|
||||
import registerClickDrag from 'aframe-click-drag-component';
|
||||
registerClickDrag(aframe);
|
||||
```
|
1
dist/.gitkeep
vendored
Normal file
1
dist/.gitkeep
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
`npm run dist` to generate browser files.
|
21
examples/basic/index.html
Normal file
21
examples/basic/index.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>A-Frame Click & Drag Component - Basic</title>
|
||||
<script src="../build.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<a-scene>
|
||||
|
||||
<a-sphere click-drag position="0 1.25 -1" radius="1.25" color="#EF2D5E"></a-sphere>
|
||||
<a-box click-drag position="-1 0.5 1" rotation="0 45 0" width="1" height="1" depth="1" color="#4CC3D9"></a-box>
|
||||
<a-cylinder click-drag position="1 0.75 1" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
|
||||
<a-plane rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane>
|
||||
|
||||
<a-sky color="#ECECEC"></a-sky>
|
||||
|
||||
<a-entity position="0 0 3.8">
|
||||
<a-camera look-controls-enabled="false"></a-camera>
|
||||
</a-entity>
|
||||
</a-scene>
|
||||
</body>
|
||||
</html>
|
37
examples/index.html
Normal file
37
examples/index.html
Normal file
|
@ -0,0 +1,37 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>A-Frame Click & Drag Component</title>
|
||||
<style>
|
||||
html {
|
||||
background: #33425B;
|
||||
color: #FAFAFA;
|
||||
font-family: monospace;
|
||||
font-size: 20px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
h1 {
|
||||
font-weight: 300;
|
||||
}
|
||||
a {
|
||||
color: #FAFAFA;
|
||||
display: block;
|
||||
padding: 15px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>A-Frame Click & Drag Component</h1>
|
||||
<a href="basic/">Demo</a>
|
||||
<p>Click + Drag entities on the screen. Note the plane cannot be dragged (it does not have the "click-drag" attribute).</p>
|
||||
<p>Try the WASD keys to move around while dragging an entity!</p>
|
||||
|
||||
<!-- GitHub Corner. -->
|
||||
<a href="https://github.com/jesstelford/aframe-click-drag-component" class="github-corner">
|
||||
<svg width="80" height="80" viewBox="0 0 250 250" style="fill:#222; color:#fff; position: absolute; top: 0; border: 0; right: 0;">
|
||||
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
3
examples/main.js
Normal file
3
examples/main.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import aframe from 'aframe';
|
||||
import clickDragComponent from '../src/index';
|
||||
clickDragComponent(aframe);
|
74
package.json
Normal file
74
package.json
Normal file
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"name": "aframe-click-drag-component",
|
||||
"version": "0.0.0",
|
||||
"description": "Click & Drag component for A-Frame.",
|
||||
"main": "lib/index.js",
|
||||
"browser": "dist/aframe-click-drag-component.min.js",
|
||||
"scripts": {
|
||||
"build-example": "browserify examples/main.js --debug --verbose -t babelify -t [envify --NODE_ENV development ] -o examples/build.js",
|
||||
"build-lib": "mkdir -p lib && babel src/index.js -o lib/build.js",
|
||||
"dist": "browserify src/index.js --verbose --debug --standalone registerAframeClickDragComponent -g uglifyify -t rollupify -t babelify -t [envify --NODE_ENV production ] | exorcist dist/out.map > dist/out.js && uglifyjs dist/out.js --screw-ie8 -c -m --in-source-map dist/out.map --source-map dist/aframe-click-drag-component.min.js.map --source-map-url aframe-click-drag-component.min.js.map > dist/aframe-click-drag-component.min.js && rm dist/out*",
|
||||
"test": "npm run test:lint",
|
||||
"test:lint": "eslint .",
|
||||
"start": "budo examples/main.js:../build.js --serve build.js --dir examples --port 8000 --live --open -- --debug --verbose -t babelify -t [envify --NODE_ENV development ]",
|
||||
"prepublish": "in-publish && npm run dist && npm run build-lib || not-in-publish",
|
||||
"preghpages": "npm run build-example && rm -rf gh-pages && mkdir gh-pages && cp -r examples/* gh-pages",
|
||||
"ghpages": "npm run preghpages && ghpages -p gh-pages"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/jesstelford/aframe-click-drag-component.git"
|
||||
},
|
||||
"keywords": [
|
||||
"aframe",
|
||||
"aframe-component",
|
||||
"aframe-vr",
|
||||
"vr",
|
||||
"mozvr",
|
||||
"webvr"
|
||||
],
|
||||
"author": "Jess Telford <hi@jes.st>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/jesstelford/aframe-click-drag-component/issues"
|
||||
},
|
||||
"homepage": "https://github.com/jesstelford/aframe-click-drag-component#readme",
|
||||
"peerDependencies": {
|
||||
"aframe": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aframe": "^0.3.0",
|
||||
"babel-cli": "^6.14.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.8.0",
|
||||
"babel-preset-es2015": "^6.9.0",
|
||||
"babel-preset-stage-0": "^6.5.0",
|
||||
"babelify": "^7.3.0",
|
||||
"browserify": "^13.1.0",
|
||||
"browserify-css": "^0.9.1",
|
||||
"budo": "^9.2.0",
|
||||
"envify": "^3.4.1",
|
||||
"eslint": "^3.2.2",
|
||||
"eslint-config-airbnb": "^10.0.0",
|
||||
"eslint-plugin-import": "^1.12.0",
|
||||
"eslint-plugin-jsx-a11y": "^2.0.1",
|
||||
"eslint-plugin-react": "^6.2.0",
|
||||
"exorcist": "^0.4.0",
|
||||
"ghpages": "^0.0.8",
|
||||
"in-publish": "^2.0.0",
|
||||
"rollupify": "^0.3.4",
|
||||
"uglify-js": "^2.7.3",
|
||||
"uglifyify": "^3.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"deep-equal": "^1.0.1"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"es2015",
|
||||
"stage-0"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-object-rest-spread"
|
||||
]
|
||||
}
|
||||
}
|
468
src/index.js
Normal file
468
src/index.js
Normal file
|
@ -0,0 +1,468 @@
|
|||
import deepEqual from 'deep-equal';
|
||||
|
||||
const COMPONENT_NAME = 'click-drag';
|
||||
|
||||
function cameraPositionToVec3(camera, vec3) {
|
||||
|
||||
let element = camera;
|
||||
|
||||
vec3.set(
|
||||
element.components.position.data.x,
|
||||
element.components.position.data.y,
|
||||
element.components.position.data.z
|
||||
);
|
||||
|
||||
while (element.attachedToParent) {
|
||||
|
||||
element = element.parentElement;
|
||||
|
||||
if (element.components && element.components.position) {
|
||||
vec3.set(
|
||||
vec3.x + element.components.position.data.x,
|
||||
vec3.y + element.components.position.data.y,
|
||||
vec3.z + element.components.position.data.z
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const {unproject} = (function unprojectFunction() {
|
||||
|
||||
let initialized = false;
|
||||
|
||||
let cameraPosition;
|
||||
|
||||
let cameraWorld;
|
||||
let matrix;
|
||||
|
||||
function initialize(THREE) {
|
||||
cameraPosition = new THREE.Vector3();
|
||||
|
||||
cameraWorld = new THREE.Matrix4();
|
||||
matrix = new THREE.Matrix4();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
unproject(THREE, vector, camera) {
|
||||
|
||||
initialized = initialized || initialize(THREE);
|
||||
|
||||
cameraPositionToVec3(camera, cameraPosition);
|
||||
|
||||
cameraWorld.identity();
|
||||
cameraWorld.setPosition(cameraPosition);
|
||||
|
||||
matrix.multiplyMatrices(
|
||||
cameraWorld,
|
||||
matrix.getInverse(camera.components.camera.camera.projectionMatrix)
|
||||
);
|
||||
|
||||
return vector.applyProjection(matrix);
|
||||
|
||||
},
|
||||
};
|
||||
}());
|
||||
|
||||
function clientCoordsTo3DCanvasCoords(
|
||||
clientX,
|
||||
clientY,
|
||||
offsetX,
|
||||
offsetY,
|
||||
clientWidth,
|
||||
clientHeight
|
||||
) {
|
||||
return {
|
||||
x: (((clientX - offsetX) / clientWidth) * 2) - 1,
|
||||
y: (-((clientY - offsetY) / clientHeight) * 2) + 1,
|
||||
};
|
||||
}
|
||||
|
||||
const {screenCoordsToDirection} = (function screenCoordsToDirectionFunction() {
|
||||
|
||||
let initialized = false;
|
||||
|
||||
let mousePosAsVec3;
|
||||
let cameraPosAsVec3;
|
||||
|
||||
function initialize(THREE) {
|
||||
mousePosAsVec3 = new THREE.Vector3();
|
||||
cameraPosAsVec3 = new THREE.Vector3();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return {
|
||||
screenCoordsToDirection(
|
||||
THREE,
|
||||
aframeCamera,
|
||||
{x: clientX, y: clientY}
|
||||
) {
|
||||
|
||||
initialized = initialized || initialize(THREE);
|
||||
|
||||
// scale mouse coordinates down to -1 <-> +1
|
||||
const {x: mouseX, y: mouseY} = clientCoordsTo3DCanvasCoords(
|
||||
clientX, clientY,
|
||||
0, 0, // TODO: Replace with canvas position
|
||||
window.innerWidth,
|
||||
window.innerHeight
|
||||
);
|
||||
|
||||
mousePosAsVec3.set(mouseX, mouseY, -1);
|
||||
|
||||
// apply camera transformation from near-plane of mouse x/y into 3d space
|
||||
// NOTE: This should be replaced with THREE code directly once the aframe bug
|
||||
// is fixed:
|
||||
// const projectedVector = new THREE
|
||||
// .Vector3(mouseX, mouseY, -1)
|
||||
// .unproject(threeCamera);
|
||||
const projectedVector = unproject(THREE, mousePosAsVec3, aframeCamera);
|
||||
|
||||
cameraPositionToVec3(aframeCamera, cameraPosAsVec3);
|
||||
|
||||
// Get the unit length direction vector from the camera's position
|
||||
const {x, y, z} = projectedVector.sub(cameraPosAsVec3).normalize();
|
||||
|
||||
return {x, y, z};
|
||||
},
|
||||
};
|
||||
}());
|
||||
|
||||
/**
|
||||
* @param planeNormal {THREE.Vector3}
|
||||
* @param planeConstant {Float} Distance from origin of the plane
|
||||
* @param rayDirection {THREE.Vector3} Direction of ray from the origin
|
||||
*
|
||||
* @return {THREE.Vector3} The intersection point of the ray and plane
|
||||
*/
|
||||
function rayPlaneIntersection(planeNormal, planeConstant, rayDirection) {
|
||||
// A line from the camera position toward (and through) the plane
|
||||
const distanceToPlane = planeConstant / planeNormal.dot(rayDirection);
|
||||
return rayDirection.multiplyScalar(distanceToPlane);
|
||||
}
|
||||
|
||||
const {directionToWorldCoords} = (function directionToWorldCoordsFunction() {
|
||||
|
||||
let initialized = false;
|
||||
|
||||
let direction;
|
||||
let cameraPosAsVec3;
|
||||
|
||||
function initialize(THREE) {
|
||||
direction = new THREE.Vector3();
|
||||
cameraPosAsVec3 = new THREE.Vector3();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* @param camera Three.js Camera instance
|
||||
* @param Object Position of the camera
|
||||
* @param Object position of the mouse (scaled to be between -1 to 1)
|
||||
* @param depth Depth into the screen to calculate world coordinates for
|
||||
*/
|
||||
directionToWorldCoords(
|
||||
THREE,
|
||||
aframeCamera,
|
||||
camera,
|
||||
{x: directionX, y: directionY, z: directionZ},
|
||||
depth
|
||||
) {
|
||||
|
||||
initialized = initialized || initialize(THREE);
|
||||
|
||||
cameraPositionToVec3(aframeCamera, cameraPosAsVec3);
|
||||
direction.set(directionX, directionY, directionZ);
|
||||
|
||||
// A line from the camera position toward (and through) the plane
|
||||
const newPosition = rayPlaneIntersection(
|
||||
camera.getWorldDirection(),
|
||||
depth,
|
||||
direction
|
||||
);
|
||||
|
||||
// Reposition back to the camera position
|
||||
const {x, y, z} = newPosition.add(cameraPosAsVec3);
|
||||
|
||||
return {x, y, z};
|
||||
|
||||
},
|
||||
};
|
||||
}());
|
||||
|
||||
const {selectItem} = (function selectItemFunction() {
|
||||
|
||||
let initialized = false;
|
||||
|
||||
let cameraPosAsVec3;
|
||||
let directionAsVec3;
|
||||
let raycaster;
|
||||
let plane;
|
||||
|
||||
function initialize(THREE) {
|
||||
plane = new THREE.Plane();
|
||||
cameraPosAsVec3 = new THREE.Vector3();
|
||||
directionAsVec3 = new THREE.Vector3();
|
||||
raycaster = new THREE.Raycaster();
|
||||
|
||||
// TODO: From camera values?
|
||||
raycaster.far = Infinity;
|
||||
raycaster.near = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return {
|
||||
selectItem(THREE, selector, camera, clientX, clientY) {
|
||||
|
||||
initialized = initialized || initialize(THREE);
|
||||
|
||||
const {x: directionX, y: directionY, z: directionZ} = screenCoordsToDirection(
|
||||
THREE,
|
||||
camera,
|
||||
{x: clientX, y: clientY}
|
||||
);
|
||||
|
||||
cameraPositionToVec3(camera, cameraPosAsVec3);
|
||||
directionAsVec3.set(directionX, directionY, directionZ);
|
||||
|
||||
raycaster.set(cameraPosAsVec3, directionAsVec3);
|
||||
|
||||
// Push meshes onto list of objects to intersect.
|
||||
// TODO: Can we do this at some other point instead of every time a ray is
|
||||
// cast? Is that a micro optimization?
|
||||
const objects = Array.from(
|
||||
camera.sceneEl.querySelectorAll(`[${selector}]`)
|
||||
).map(object => object.object3D);
|
||||
|
||||
const recursive = true;
|
||||
|
||||
const intersected = raycaster
|
||||
.intersectObjects(objects, recursive)
|
||||
// Only keep intersections against objects that have a reference to an entity.
|
||||
.filter(intersection => !!intersection.object.el)
|
||||
// Only keep ones that are visible
|
||||
.filter(intersection => intersection.object.parent.visible)
|
||||
// The first element is the closest
|
||||
[0]; // eslint-disable-line no-unexpected-multiline
|
||||
|
||||
if (!intersected) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const {point, object} = intersected;
|
||||
|
||||
// Aligned to the world direction of the camera
|
||||
// At the specified intersection point
|
||||
plane.setFromNormalAndCoplanarPoint(
|
||||
camera.components.camera.camera.getWorldDirection().clone().negate(),
|
||||
point.clone().sub(cameraPosAsVec3)
|
||||
);
|
||||
|
||||
const depth = plane.constant;
|
||||
|
||||
const offset = point.sub(object.getWorldPosition());
|
||||
|
||||
return {depth, offset, element: object.el};
|
||||
|
||||
},
|
||||
};
|
||||
}());
|
||||
|
||||
function dragItem(THREE, element, offset, camera, depth, mouseInfo) {
|
||||
|
||||
const {x: offsetX, y: offsetY, z: offsetZ} = offset;
|
||||
let lastMouseInfo = mouseInfo;
|
||||
|
||||
function onMouseMove({clientX, clientY}) {
|
||||
|
||||
lastMouseInfo = {clientX, clientY};
|
||||
|
||||
const direction = screenCoordsToDirection(
|
||||
THREE,
|
||||
camera,
|
||||
{x: clientX, y: clientY}
|
||||
);
|
||||
|
||||
const {x, y, z} = directionToWorldCoords(
|
||||
THREE,
|
||||
camera,
|
||||
camera.components.camera.camera,
|
||||
direction,
|
||||
depth
|
||||
);
|
||||
|
||||
element.setAttribute('position', {x: x - offsetX, y: y - offsetY, z: z - offsetZ});
|
||||
}
|
||||
|
||||
function onCameraMove({detail}) {
|
||||
if (detail.name === 'position' && !deepEqual(detail.oldData, detail.newData)) {
|
||||
onMouseMove(lastMouseInfo);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
camera.addEventListener('componentchanged', onCameraMove);
|
||||
|
||||
// The "unlisten" function
|
||||
return _ => {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
camera.removeEventListener('componentchanged', onCameraMove);
|
||||
};
|
||||
}
|
||||
|
||||
// Closure to close over the removal of the event listeners
|
||||
const {initialize, tearDown} = (function closeOverInitAndTearDown() {
|
||||
|
||||
let removeClickListeners;
|
||||
|
||||
return {
|
||||
initialize(THREE, componentName) {
|
||||
|
||||
// TODO: Based on a scene from the element passed in?
|
||||
const scene = document.querySelector('a-scene');
|
||||
// delay loading of this as we're not 100% if the scene has loaded yet or not
|
||||
let camera;
|
||||
let removeDragListeners;
|
||||
|
||||
function onMouseDown({clientX, clientY}) {
|
||||
|
||||
const {depth, offset, element} = selectItem(THREE, componentName, camera, clientX, clientY);
|
||||
|
||||
if (element) {
|
||||
// Can only drag one item at a time, so no need to check if any
|
||||
// listener is already set up
|
||||
removeDragListeners = dragItem(THREE, element, offset, camera, depth, {clientX, clientY});
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
removeDragListeners && removeDragListeners(); // eslint-disable-line no-unused-expressions
|
||||
removeDragListeners = undefined;
|
||||
}
|
||||
|
||||
function run() {
|
||||
|
||||
camera = scene.camera.el;
|
||||
|
||||
// TODO: Attach to canvas?
|
||||
document.addEventListener('mousedown', onMouseDown);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
|
||||
removeClickListeners = _ => {
|
||||
document.removeEventListener('mousedown', onMouseDown);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
if (scene.hasLoaded) {
|
||||
run();
|
||||
} else {
|
||||
scene.addEventListener('loaded', run);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
tearDown() {
|
||||
removeClickListeners && removeClickListeners(); // eslint-disable-line no-unused-expressions
|
||||
removeClickListeners = undefined;
|
||||
},
|
||||
};
|
||||
}());
|
||||
|
||||
const {didMount, didUnmount} = (function getDidMountAndUnmount() {
|
||||
|
||||
const cache = [];
|
||||
|
||||
return {
|
||||
didMount(element, THREE, componentName) {
|
||||
|
||||
if (cache.length === 0) {
|
||||
initialize(THREE, componentName);
|
||||
}
|
||||
|
||||
if (cache.indexOf(element) === -1) {
|
||||
cache.push(element);
|
||||
}
|
||||
},
|
||||
|
||||
didUnmount(element) {
|
||||
|
||||
const cacheIndex = cache.indexOf(element);
|
||||
|
||||
if (cacheIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove that element
|
||||
cache.splice(cacheIndex, 1);
|
||||
|
||||
if (cache.length === 0) {
|
||||
tearDown();
|
||||
}
|
||||
|
||||
},
|
||||
};
|
||||
}());
|
||||
|
||||
/**
|
||||
* @param aframe {Object} The Aframe instance to register with
|
||||
* @param componentName {String} The component name to use. Default: 'click-drag'
|
||||
*/
|
||||
export default function aframeDraggableComponent(aframe, componentName = COMPONENT_NAME) {
|
||||
|
||||
const THREE = aframe.THREE;
|
||||
|
||||
/**
|
||||
* Draggable component for A-Frame.
|
||||
*/
|
||||
aframe.registerComponent(componentName, {
|
||||
schema: { },
|
||||
|
||||
/**
|
||||
* Called once when component is attached. Generally for initial setup.
|
||||
*/
|
||||
init() {
|
||||
didMount(this, THREE, componentName);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when component is attached and when component data changes.
|
||||
* Generally modifies the entity based on the data.
|
||||
*
|
||||
* @param oldData
|
||||
*/
|
||||
update() { },
|
||||
|
||||
/**
|
||||
* Called when a component is removed (e.g., via removeAttribute).
|
||||
* Generally undoes all modifications to the entity.
|
||||
*/
|
||||
remove() {
|
||||
didUnmount();
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when entity pauses.
|
||||
* Use to stop or remove any dynamic or background behavior such as events.
|
||||
*/
|
||||
pause() {
|
||||
didUnmount();
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when entity resumes.
|
||||
* Use to continue or add any dynamic or background behavior such as events.
|
||||
*/
|
||||
play() {
|
||||
didMount(this, THREE, componentName);
|
||||
},
|
||||
});
|
||||
};
|
Loading…
Reference in a new issue