diff --git a/src/core/Object3D.js b/src/core/Object3D.js index 773108f..1b4d761 100644 --- a/src/core/Object3D.js +++ b/src/core/Object3D.js @@ -1,38 +1,20 @@ -import { Vector2 } from 'three'; import { watch } from 'vue'; import { bindProp } from '../tools/index.js'; export default { name: 'Object3D', inject: ['three', 'scene', 'rendererComponent'], - emits: ['created', 'ready', 'pointerEnter', 'pointerOver', 'pointerLeave', 'click'], + emits: ['created', 'ready'], props: { position: { type: Object, default: { x: 0, y: 0, z: 0 } }, rotation: { type: Object, default: { x: 0, y: 0, z: 0 } }, scale: { type: Object, default: { x: 1, y: 1, z: 1 } }, lookAt: { type: Object, default: null }, - onPointerEnter: { type: Function, default: null }, - onPointerOver: { type: Function, default: null }, - onPointerLeave: { type: Function, default: null }, - onClick: { type: Function, default: null }, - usePointerEvents: { type: Boolean, default: false }, - pointerObjects: { type: [Boolean, Array], default: null } - }, - data() { - return { - pointerOver: null - } }, // can't use setup because it will not be used in sub components // setup() {}, unmounted() { if (this._parent) this._parent.remove(this.o3d); - - // teardown listeners - this.three.offBeforeRender(this.pointerHandler); - if (this.three.mouse_move_element) { - this.three.mouse_move_element.removeEventListener('mouseleave', this.renderElementLeaveHandler) - } }, methods: { initObject3D(o3d) { @@ -47,23 +29,6 @@ export default { if (this.lookAt) this.o3d.lookAt(this.lookAt.x, this.lookAt.y, this.lookAt.z); watch(() => this.lookAt, (v) => { this.o3d.lookAt(v.x, v.y, v.z); }, { deep: true }); - if (this.usePointerEvents - || this.onPointerEnter - || this.onPointerOver - || this.onPointerLeave - || this.onClick) { - this.three.onBeforeRender(this.pointerHandler); - } - if (this.onPointerLeave) { - // we need to wait a tick so the mouse_move_element is created - // TODO: more robust fix - this.$nextTick(() => this.three.mouse_move_element.addEventListener('mouseleave', this.renderElementLeaveHandler)); - } - if (this.onClick) { - window.addEventListener('click', this.clickHandler); - // TODO: touch - } - // find first viable parent let parent = this.$parent; while (parent) { @@ -79,103 +44,6 @@ export default { }, add(o) { this.o3d.add(o); }, remove(o) { this.o3d.remove(o); }, - pointerHandler() { - this.three.raycaster.setFromCamera(this.three.mouse, this.three.camera); - - // determine what we're raycasting against - let objectsToCastAgainst = this.pointerObjects; - if (objectsToCastAgainst) { - // cast against all objects in scene if prop is `true` - if (objectsToCastAgainst === true) { - objectsToCastAgainst = this.three.scene.children; - } - } else { - // default: just cast against this object - objectsToCastAgainst = [this.o3d]; - } - - // find all intersects - const intersects = this.three.raycaster.intersectObjects(objectsToCastAgainst); - // determine if the first intersect is this object - const match = intersects.length && - intersects[0].object.uuid === this.o3d.uuid - ? intersects[0] - : null; - - // if so, let's start the callback process - if (match) { - // pointer is newly over o3d - if (!this.pointerOver) { - this.pointerOver = true; - - if (this.onPointerEnter) { - - this.onPointerEnter({ - object: this.o3d, - intersect: match - }); - - if (this.usePointerEvents) { - this.$emit('pointerEnter', { - object: this.o3d, - intersect: match - }); - } - } - } - // pointer is still over o3d - else if (this.onPointerOver) { - this.onPointerOver({ - object: this.o3d, - intersect: match - }); - - if (this.usePointerEvents) { - this.$emit('pointerOver', { - object: this.o3d, - intersect: match - }); - } - } - } else { - // pointer is not over o3d - - // pointer has just left o3d - if (this.pointerOver) { - this.pointerOver = false; - if (this.onPointerLeave) { - this.onPointerLeave({ object: this.o3d }); - } - - if (this.usePointerEvents) { - this.$emit('pointerLeave', { - object: this.o3d - }); - } - } - } - }, - clickHandler(evt) { - if (this.pointerOver) { - // cast a ray so we can provide hit info - this.three.raycaster.setFromCamera(this.three.mouse, this.three.camera); - const [intersect] = this.three.raycaster.intersectObjects([this.o3d]); - - // callbacks and events - if (this.onClick) { - this.onClick({ object: this.o3d, intersect }); - } - if (this.usePointerEvents) { - this.$emit('click', { object: this.o3d, intersect }); - } - } - }, - renderElementLeaveHandler() { - // since the mouse is off the renderer, we'll set its values to an unreachable number - this.three.mouse.x = this.three.mouse.y = Infinity; - // then run the normal pointer handler with these updated mouse values - this.pointerHandler(); - } }, render() { return this.$slots.default ? this.$slots.default() : []; diff --git a/src/core/Renderer.js b/src/core/Renderer.js index c9ec58d..c7716a1 100644 --- a/src/core/Renderer.js +++ b/src/core/Renderer.js @@ -7,11 +7,7 @@ export default { antialias: Boolean, alpha: Boolean, autoClear: { type: Boolean, default: true }, - // mouseMove: { type: [Boolean, String], default: false }, - // mouseRaycast: { type: Boolean, default: false }, - // mouseOver: { type: Boolean, default: false }, - usePointer: { type: Boolean, default: true }, - // click: { type: Boolean, default: false }, + usePointer: { type: Boolean, default: false }, orbitCtrl: { type: [Boolean, Object], default: false }, resize: { type: [Boolean, String], default: false }, shadow: Boolean, @@ -39,11 +35,7 @@ export default { alpha: this.alpha, autoClear: this.autoClear, orbit_ctrl: this.orbitCtrl, - // mouse_move: this.mouseMove, - // mouse_raycast: this.mouseRaycast, - // mouse_over: this.mouseOver, use_pointer: this.usePointer, - // click: this.click, resize: this.resize, width: this.width, height: this.height, diff --git a/src/core/usePointer.js b/src/core/usePointer.js new file mode 100644 index 0000000..7b1f31f --- /dev/null +++ b/src/core/usePointer.js @@ -0,0 +1,139 @@ +import { Vector2, Vector3 } from 'three'; +import useRaycaster from './useRaycaster'; + +export default function usePointer(options) { + const { + camera, + domElement, + intersectObjects, + touch = true, + resetOnEnd = false, + resetPosition = new Vector2(), + resetPositionV3 = new Vector3(), + // onEnter = () => {}, + // onLeave = () => {}, + // onMove = () => {}, + // onDown = () => {}, + // onUp = () => {}, + // onClick = () => {}, + } = options; + + const position = resetPosition.clone(); + const positionN = new Vector2(); + + const raycaster = useRaycaster({ camera }); + const positionV3 = raycaster.position; + + const obj = { + position, + positionN, + positionV3, + listeners: false, + addListeners, + removeListeners, + }; + + return obj; + + function reset() { + position.copy(resetPosition); + positionV3.copy(resetPositionV3); + }; + + function updatePosition(event) { + let x, y; + if (event.touches && event.touches.length > 0) { + x = event.touches[0].clientX; + y = event.touches[0].clientY; + } else { + x = event.clientX; + y = event.clientY; + } + + const rect = domElement.getBoundingClientRect(); + position.x = x - rect.left; + position.y = y - rect.top; + positionN.x = (position.x / rect.width) * 2 - 1; + positionN.y = (position.y / rect.height) * 2 - 1; + raycaster.updatePosition(positionN); + }; + + function pointerEnter(event) { + updatePosition(event); + // onEnter(); + }; + + function pointerChange() { + if (intersectObjects.length) { + const intersects = raycaster.intersect(positionN, intersectObjects); + const offObjects = [...intersectObjects]; + + intersects.forEach(intersect => { + const { object } = intersect; + const { component } = object; + if (!object.over) { + object.over = true; + if (component.onPointerOver) component.onPointerOver({ over: true, component, intersect }); + if (component.onPointerEnter) component.onPointerEnter({ component, intersect }); + } + offObjects.splice(offObjects.indexOf(object), 1); + }); + + offObjects.forEach(object => { + const { component } = object; + if (object.over && component.onPointerOver) { + object.over = false; + if (component.onPointerOver) component.onPointerOver({ over: false, component }); + if (component.onPointerLeave) component.onPointerLeave({ component }); + } + }); + } + }; + + function pointerMove(event) { + updatePosition(event); + pointerChange(); + // onMove(); + }; + + function pointerClick(event) { + updatePosition(event); + if (intersectObjects.length) { + const intersects = raycaster.intersect(positionN, intersectObjects); + intersects.forEach(intersect => { + const { object } = intersect; + const { component } = object; + if (component.onClick) component.onClick({ component, intersect }); + }); + } + }; + + function pointerLeave(event) { + if (resetOnEnd) reset(); + // onLeave(); + }; + + function addListeners() { + domElement.addEventListener('mouseenter', pointerEnter); + domElement.addEventListener('mousemove', pointerMove); + domElement.addEventListener('mouseleave', pointerLeave); + domElement.addEventListener('click', pointerClick); + if (touch) { + domElement.addEventListener('touchstart', pointerEnter); + domElement.addEventListener('touchmove', pointerMove); + domElement.addEventListener('touchend', pointerLeave); + } + obj.listeners = true; + }; + + function removeListeners() { + domElement.removeEventListener('mouseenter', pointerEnter); + domElement.removeEventListener('mousemove', pointerMove); + domElement.removeEventListener('mouseleave', pointerLeave); + + domElement.removeEventListener('touchstart', pointerEnter); + domElement.removeEventListener('touchmove', pointerMove); + domElement.removeEventListener('touchend', pointerLeave); + obj.listeners = false; + }; +}; diff --git a/src/core/useRaycaster.js b/src/core/useRaycaster.js new file mode 100644 index 0000000..6d8b62e --- /dev/null +++ b/src/core/useRaycaster.js @@ -0,0 +1,29 @@ +import { Plane, Raycaster, Vector3 } from 'three'; + +export default function useRaycaster(options) { + const { + camera, + resetPosition = new Vector3(0, 0, 0), + } = options; + + const raycaster = new Raycaster(); + const position = resetPosition.clone(); + const plane = new Plane(new Vector3(0, 0, 1), 0); + + const updatePosition = (coords) => { + raycaster.setFromCamera(coords, camera); + camera.getWorldDirection(plane.normal); + raycaster.ray.intersectPlane(plane, position); + }; + + const intersect = (coords, objects) => { + raycaster.setFromCamera(coords, camera); + return raycaster.intersectObjects(objects); + }; + + return { + position, + updatePosition, + intersect, + }; +}; diff --git a/src/core/useThree.js b/src/core/useThree.js index 90b9973..f87a5b9 100644 --- a/src/core/useThree.js +++ b/src/core/useThree.js @@ -1,13 +1,11 @@ import { - Plane, - Raycaster, - Vector2, - Vector3, WebGLRenderer, } from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; +import usePointer from './usePointer'; + /** * Three.js helper */ @@ -19,14 +17,10 @@ export default function useThree() { alpha: false, autoClear: true, orbit_ctrl: false, - // mouse_move: false, - // mouse_raycast: false, - // mouse_over: false, - use_pointer: true, - // click: false, - resize: true, - width: 0, - height: 0, + use_pointer: false, + resize: false, + width: 300, + height: 150, }; // size @@ -41,26 +35,16 @@ export default function useThree() { let afterResizeCallbacks = []; let beforeRenderCallbacks = []; - // mouse tracking - const mouse = new Vector2(Infinity, Infinity); - const mouseV3 = new Vector3(); - const mousePlane = new Plane(new Vector3(0, 0, 1), 0); - const raycaster = new Raycaster(); - - // raycast objects - const intersectObjects = []; - // returned object const obj = { conf, renderer: null, camera: null, cameraCtrl: null, - materials: {}, scene: null, + pointer: null, + intersectObjects: [], size, - mouse, mouseV3, - raycaster, init, dispose, render, @@ -95,6 +79,15 @@ export default function useThree() { obj.renderer = new WebGLRenderer({ canvas: conf.canvas, antialias: conf.antialias, alpha: conf.alpha }); obj.renderer.autoClear = conf.autoClear; + if (conf.resize) { + onResize(); + window.addEventListener('resize', onResize); + } else { + setSize(conf.width, conf.height); + } + + initPointer(); + if (conf.orbit_ctrl) { obj.orbitCtrl = new OrbitControls(obj.camera, obj.renderer.domElement); if (conf.orbit_ctrl instanceof Object) { @@ -104,36 +97,23 @@ export default function useThree() { } } - if (conf.resize) { - onResize(); - window.addEventListener('resize', onResize); - } else { - setSize(conf.width | 300, conf.height | 150); - } - - // conf.mouse_move = conf.mouse_move || conf.mouse_over; - if (conf.use_pointer) { - if (conf.use_pointer === true) { - // use renderer element as mousemove by default - obj.mouse_move_element = obj.renderer.domElement; - } else { - // use custom element as mousemove element - obj.mouse_move_element = conf.use_pointer; - } - obj.mouse_move_element.addEventListener('mousemove', onMousemove); - obj.mouse_move_element.addEventListener('mouseleave', onMouseleave); - // TODO: touch - } - - // if (conf.click) { - // obj.renderer.domElement.addEventListener('click', onClick); - // } - afterInitCallbacks.forEach(c => c()); return true; }; + function initPointer() { + obj.pointer = usePointer({ + camera: obj.camera, + domElement: obj.renderer.domElement, + intersectObjects: obj.intersectObjects, + }); + + if (conf.use_pointer || obj.intersectObjects.length) { + obj.pointer.addListeners(); + } + } + /** * add after init callback */ @@ -191,8 +171,12 @@ export default function useThree() { * add intersect object */ function addIntersectObject(o) { - if (intersectObjects.indexOf(o) === -1) { - intersectObjects.push(o); + if (obj.intersectObjects.indexOf(o) === -1) { + obj.intersectObjects.push(o); + } + // add listeners if needed + if (obj.pointer && !obj.pointer.listeners) { + obj.pointer.addListeners(); } } @@ -200,9 +184,13 @@ export default function useThree() { * remove intersect object */ function removeIntersectObject(o) { - const i = intersectObjects.indexOf(o); + const i = obj.intersectObjects.indexOf(o); if (i !== -1) { - intersectObjects.splice(i, 1); + obj.intersectObjects.splice(i, 1); + } + // remove listeners if needed + if (obj.pointer && !conf.use_pointer && obj.intersectObjects.length === 0) { + obj.pointer.removeListeners(); } } @@ -212,50 +200,9 @@ export default function useThree() { function dispose() { beforeRenderCallbacks = []; window.removeEventListener('resize', onResize); - if (obj.mouse_move_element) { - obj.mouse_move_element.removeEventListener('mousemove', onMousemove); - obj.mouse_move_element.removeEventListener('mouseleave', onMouseleave); - } - // obj.renderer.domElement.removeEventListener('click', onClick); - // TODO: touch + if (obj.pointer) obj.pointer.removeListeners(); if (obj.orbitCtrl) obj.orbitCtrl.dispose(); - this.renderer.dispose(); - } - - /** - */ - function updateMouse(e) { - const rect = obj.mouse_move_element.getBoundingClientRect(); - mouse.x = ((e.x - rect.left) / size.width) * 2 - 1; - mouse.y = -((e.y - rect.top) / size.height) * 2 + 1; - } - - /** - * click listener - */ - // function onClick(e) { - // updateMouse(e); - // raycaster.setFromCamera(mouse, obj.camera); - // const objects = raycaster.intersectObjects(intersectObjects); - // for (let i = 0; i < objects.length; i++) { - // const o = objects[i].object; - // if (o.onClick) o.onClick(e); - // } - // } - - /** - * mousemove listener - */ - function onMousemove(e) { - updateMouse(e); - } - - /** - * mouseleave listener - */ - function onMouseleave(e) { - mouse.x = Infinity; - mouse.y = Infinity; + obj.renderer.dispose(); } /** diff --git a/src/meshes/Mesh.js b/src/meshes/Mesh.js index 086df15..f1864ad 100644 --- a/src/meshes/Mesh.js +++ b/src/meshes/Mesh.js @@ -1,14 +1,19 @@ import { watch } from 'vue'; import { Mesh } from 'three'; import Object3D from '../core/Object3D.js'; +import { bindProp } from '../tools'; export default { - extends: Object3D, name: 'Mesh', + extends: Object3D, props: { castShadow: Boolean, receiveShadow: Boolean, - onHover: Function, + onPointerEnter: Function, + onPointerOver: Function, + onPointerLeave: Function, + onPointerDown: Function, + onPointerUp: Function, onClick: Function, }, // can't use setup because it will not be used in sub components @@ -24,19 +29,17 @@ export default { methods: { initMesh() { this.mesh = new Mesh(this.geometry, this.material); + this.mesh.component = this; - ['castShadow', 'receiveShadow'].forEach(p => { - this.mesh[p] = this[p]; - watch(() => this[p], () => { this.mesh[p] = this[p]; }); - }); + bindProp(this, 'castShadow', this.mesh); + bindProp(this, 'receiveShadow', this.mesh); - if (this.onHover) { - this.mesh.onHover = (over) => { this.onHover({ component: this, over }); }; - this.three.addIntersectObject(this.mesh); - } - - if (this.onClick) { - this.mesh.onClick = (e) => { this.onClick({ component: this, event: e }); }; + if (this.onPointerEnter || + this.onPointerOver || + this.onPointerLeave || + this.onPointerDown || + this.onPointerUp || + this.onClick) { this.three.addIntersectObject(this.mesh); }