UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

449 lines (446 loc) 15.7 kB
import { Debug } from '../../core/debug.js'; import { math } from '../../core/math/math.js'; import { Vec3 } from '../../core/math/vec3.js'; import { Mat4 } from '../../core/math/mat4.js'; import { Ray } from '../../core/shape/ray.js'; import { EventHandler } from '../../core/event-handler.js'; import { CameraComponent } from '../../framework/components/camera/component.js'; import { SORTMODE_NONE, PROJECTION_PERSPECTIVE } from '../../scene/constants.js'; import { Entity } from '../../framework/entity.js'; import { Layer } from '../../scene/layer.js'; import { GIZMOSPACE_WORLD, GIZMOSPACE_LOCAL } from './constants.js'; /** * @import { AppBase } from '../../framework/app-base.js' * @import { GraphNode } from '../../scene/graph-node.js' * @import { GraphicsDevice } from '../../platform/graphics/graphics-device.js' * @import { MeshInstance } from '../../scene/mesh-instance.js' * @import { Shape } from './shape/shape.js' */ // temporary variables var tmpV1 = new Vec3(); var tmpV2 = new Vec3(); var tmpM1 = new Mat4(); var tmpM2 = new Mat4(); var tmpR1 = new Ray(); // constants var LAYER_NAME = 'Gizmo'; var MIN_SCALE = 1e-4; var PERS_SCALE_RATIO = 0.3; var ORTHO_SCALE_RATIO = 0.32; var UPDATE_EPSILON = 1e-6; /** * The base class for all gizmos. * * @category Gizmo */ class Gizmo extends EventHandler { /** * Creates a new gizmo layer and adds it to the scene. * * @param {AppBase} app - The app. * @param {string} [layerName] - The layer name. Defaults to 'Gizmo'. * @param {number} [layerIndex] - The layer index. Defaults to the end of the layer list. * @returns {Layer} The new layer. */ static createLayer(app, layerName, layerIndex) { if (layerName === void 0) layerName = LAYER_NAME; var layer = new Layer({ name: layerName, clearDepthBuffer: true, opaqueSortMode: SORTMODE_NONE, transparentSortMode: SORTMODE_NONE }); app.scene.layers.insert(layer, layerIndex != null ? layerIndex : app.scene.layers.layerList.length); return layer; } /** * Sets the gizmo render layer. * * @type {Layer} */ get layer() { return this._layer; } /** * Sets the gizmo coordinate space. Can be: * * - {@link GIZMOSPACE_LOCAL} * - {@link GIZMOSPACE_WORLD} * * Defaults to {@link GIZMOSPACE_WORLD}. * * @type {string} */ set coordSpace(value) { this._coordSpace = value != null ? value : GIZMOSPACE_WORLD; this._updateRotation(); } /** * Gets the gizmo coordinate space. * * @type {string} */ get coordSpace() { return this._coordSpace; } /** * Sets the gizmo size. Defaults to 1. * * @type {number} */ set size(value) { this._size = value; this._updateScale(); } /** * Gets the gizmo size. * * @type {number} */ get size() { return this._size; } /** * @type {Vec3} * @protected */ get facing() { if (this._camera.projection === PROJECTION_PERSPECTIVE) { var gizmoPos = this.root.getPosition(); var cameraPos = this._camera.entity.getPosition(); return tmpV2.sub2(cameraPos, gizmoPos).normalize(); } return tmpV2.copy(this._camera.entity.forward).mulScalar(-1); } /** * @param {PointerEvent} e - The pointer event. * @private */ _onPointerDown(e) { if (!this.root.enabled || document.pointerLockElement) { return; } var selection = this._getSelection(e.offsetX, e.offsetY); if (selection[0]) { e.preventDefault(); e.stopPropagation(); } // capture the pointer during drag var { canvas } = this._device; canvas.setPointerCapture(e.pointerId); this.fire(Gizmo.EVENT_POINTERDOWN, e.offsetX, e.offsetY, selection[0]); } /** * @param {PointerEvent} e - The pointer event. * @private */ _onPointerMove(e) { if (!this.root.enabled || document.pointerLockElement) { return; } var selection = this._getSelection(e.offsetX, e.offsetY); if (selection[0]) { e.preventDefault(); e.stopPropagation(); } this.fire(Gizmo.EVENT_POINTERMOVE, e.offsetX, e.offsetY, selection[0]); } /** * @param {PointerEvent} e - The pointer event. * @private */ _onPointerUp(e) { if (!this.root.enabled || document.pointerLockElement) { return; } var selection = this._getSelection(e.offsetX, e.offsetY); if (selection[0]) { e.preventDefault(); e.stopPropagation(); } var { canvas } = this._device; canvas.releasePointerCapture(e.pointerId); this.fire(Gizmo.EVENT_POINTERUP, e.offsetX, e.offsetY, selection[0]); } /** * @protected */ _updatePosition() { tmpV1.set(0, 0, 0); for(var i = 0; i < this.nodes.length; i++){ var node = this.nodes[i]; tmpV1.add(node.getPosition()); } tmpV1.mulScalar(1.0 / (this.nodes.length || 1)); if (tmpV1.distance(this.root.getPosition()) < UPDATE_EPSILON) { return; } this.root.setPosition(tmpV1); this.fire(Gizmo.EVENT_POSITIONUPDATE, tmpV1); } /** * @protected */ _updateRotation() { tmpV1.set(0, 0, 0); if (this._coordSpace === GIZMOSPACE_LOCAL && this.nodes.length !== 0) { tmpV1.copy(this.nodes[this.nodes.length - 1].getEulerAngles()); } if (tmpV1.distance(this.root.getEulerAngles()) < UPDATE_EPSILON) { return; } this.root.setEulerAngles(tmpV1); this.fire(Gizmo.EVENT_ROTATIONUPDATE, tmpV1); } /** * @protected */ _updateScale() { if (this._camera.projection === PROJECTION_PERSPECTIVE) { var gizmoPos = this.root.getPosition(); var cameraPos = this._camera.entity.getPosition(); var dist = gizmoPos.distance(cameraPos); this._scale = Math.tan(0.5 * this._camera.fov * math.DEG_TO_RAD) * dist * PERS_SCALE_RATIO; } else { this._scale = this._camera.orthoHeight * ORTHO_SCALE_RATIO; } this._scale = Math.max(this._scale * this._size, MIN_SCALE); if (Math.abs(this._scale - this.root.getLocalScale().x) < UPDATE_EPSILON) { return; } this.root.setLocalScale(this._scale, this._scale, this._scale); this.fire(Gizmo.EVENT_SCALEUPDATE, this._scale); } /** * @param {number} x - The x coordinate. * @param {number} y - The y coordinate. * @returns {MeshInstance[]} - The mesh instances. * @private */ _getSelection(x, y) { var start = this._camera.screenToWorld(x, y, 0); var end = this._camera.screenToWorld(x, y, this._camera.farClip - this._camera.nearClip); var dir = tmpV1.copy(end).sub(start).normalize(); var selection = []; for(var i = 0; i < this.intersectShapes.length; i++){ var shape = this.intersectShapes[i]; if (shape.disabled || !shape.entity.enabled) { continue; } var parentTM = shape.entity.getWorldTransform(); for(var j = 0; j < shape.triData.length; j++){ var { tris, transform, priority } = shape.triData[j]; // combine node world transform with transform of tri relative to parent var triWTM = tmpM1.copy(parentTM).mul(transform); var invTriWTM = tmpM2.copy(triWTM).invert(); var ray = tmpR1; invTriWTM.transformPoint(start, ray.origin); invTriWTM.transformVector(dir, ray.direction); ray.direction.normalize(); for(var k = 0; k < tris.length; k++){ if (tris[k].intersectsRay(ray, tmpV1)) { selection.push({ dist: triWTM.transformPoint(tmpV1).sub(start).length(), meshInstances: shape.meshInstances, priority: priority }); } } } } if (selection.length) { selection.sort((s0, s1)=>{ if (s0.priority !== 0 && s1.priority !== 0) { return s1.priority - s0.priority; } return s0.dist - s1.dist; }); return selection[0].meshInstances; } return []; } /** * Attach an array of graph nodes to the gizmo. * * @param {GraphNode[] | GraphNode} [nodes] - The graph nodes. Defaults to []. * @example * const gizmo = new pc.Gizmo(camera, layer); * gizmo.attach([boxA, boxB]); */ attach(nodes) { if (nodes === void 0) nodes = []; if (Array.isArray(nodes)) { if (nodes.length === 0) { return; } this.nodes = nodes; } else { this.nodes = [ nodes ]; } this._updatePosition(); this._updateRotation(); this._updateScale(); this.fire(Gizmo.EVENT_NODESATTACH); this.root.enabled = true; this.fire(Gizmo.EVENT_RENDERUPDATE); } /** * Detaches all graph nodes from the gizmo. * * @example * const gizmo = new pc.Gizmo(camera, layer); * gizmo.attach([boxA, boxB]); * gizmo.detach(); */ detach() { this.root.enabled = false; this.fire(Gizmo.EVENT_RENDERUPDATE); this.fire(Gizmo.EVENT_NODESDETACH); this.nodes = []; } /** * Detaches all graph nodes and destroys the gizmo instance. * * @example * const gizmo = new pc.Gizmo(camera, layer); * gizmo.attach([boxA, boxB]); * gizmo.destroy(); */ destroy() { this.detach(); this._device.canvas.removeEventListener('pointerdown', this._onPointerDown); this._device.canvas.removeEventListener('pointermove', this._onPointerMove); this._device.canvas.removeEventListener('pointerup', this._onPointerUp); this.root.destroy(); } /** * Creates a new Gizmo object. * * @param {CameraComponent} camera - The camera component. * @param {Layer} layer - The render layer. This can be provided by the user or will be created * and added to the scene and camera if not provided. Successive gizmos will share the same layer * and will be removed from the camera and scene when the last gizmo is destroyed. * const gizmo = new pc.Gizmo(camera, layer); */ constructor(camera, layer){ Debug.assert(camera instanceof CameraComponent, 'Incorrect parameters for Gizmos\'s constructor. Use new Gizmo(camera, layer)'); super(), /** * Internal version of the gizmo size. Defaults to 1. * * @type {number} * @private */ this._size = 1, /** * Internal version of the gizmo scale. Defaults to 1. * * @type {number} * @protected */ this._scale = 1, /** * Internal version of coordinate space. Defaults to {@link GIZMOSPACE_WORLD}. * * @type {string} * @protected */ this._coordSpace = GIZMOSPACE_WORLD, /** * The graph nodes attached to the gizmo. * * @type {GraphNode[]} */ this.nodes = [], /** * The intersection shapes for the gizmo. * * @type {Shape[]} */ this.intersectShapes = []; this._camera = camera; this._app = camera.system.app; this._device = this._app.graphicsDevice; this._layer = layer; camera.layers = camera.layers.concat(layer.id); this.root = new Entity('gizmo'); this._app.root.addChild(this.root); this.root.enabled = false; this._updateScale(); this._onPointerDown = this._onPointerDown.bind(this); this._onPointerMove = this._onPointerMove.bind(this); this._onPointerUp = this._onPointerUp.bind(this); this._device.canvas.addEventListener('pointerdown', this._onPointerDown); this._device.canvas.addEventListener('pointermove', this._onPointerMove); this._device.canvas.addEventListener('pointerup', this._onPointerUp); this._app.on('update', ()=>{ this._updatePosition(); this._updateRotation(); this._updateScale(); }); this._app.on('destroy', ()=>this.destroy()); } } /** * Fired when the pointer is down on the gizmo. * * @event * @example * const gizmo = new pc.Gizmo(camera, layer); * gizmo.on('pointer:down', (x, y, meshInstance) => { * console.log(`Pointer was down on ${meshInstance.node.name} at ${x}, ${y}`); * }); */ Gizmo.EVENT_POINTERDOWN = 'pointer:down'; /** * Fired when the pointer is moving over the gizmo. * * @event * @example * const gizmo = new pc.Gizmo(camera, layer); * gizmo.on('pointer:move', (x, y, meshInstance) => { * console.log(`Pointer was moving on ${meshInstance.node.name} at ${x}, ${y}`); * }); */ Gizmo.EVENT_POINTERMOVE = 'pointer:move'; /** * Fired when the pointer is up off the gizmo. * * @event * @example * const gizmo = new pc.Gizmo(camera, layer); * gizmo.on('pointer:up', (x, y, meshInstance) => { * console.log(`Pointer was up on ${meshInstance.node.name} at ${x}, ${y}`); * }) */ Gizmo.EVENT_POINTERUP = 'pointer:up'; /** * Fired when the gizmo's position is updated. * * @event * @example * const gizmo = new pc.Gizmo(camera, layer); * gizmo.on('position:update', (position) => { * console.log(`The gizmo's position was updated to ${position}`); * }) */ Gizmo.EVENT_POSITIONUPDATE = 'position:update'; /** * Fired when the gizmo's rotation is updated. * * @event * @example * const gizmo = new pc.Gizmo(camera, layer); * gizmo.on('rotation:update', (rotation) => { * console.log(`The gizmo's rotation was updated to ${rotation}`); * }); */ Gizmo.EVENT_ROTATIONUPDATE = 'rotation:update'; /** * Fired when the gizmo's scale is updated. * * @event * @example * const gizmo = new pc.Gizmo(camera, layer); * gizmo.on('scale:update', (scale) => { * console.log(`The gizmo's scale was updated to ${scale}`); * }); */ Gizmo.EVENT_SCALEUPDATE = 'scale:update'; /** * Fired when graph nodes are attached. * * @event * @example * const gizmo = new pc.Gizmo(camera, layer); * gizmo.on('nodes:attach', () => { * console.log('Graph nodes attached'); * }); */ Gizmo.EVENT_NODESATTACH = 'nodes:attach'; /** * Fired when graph nodes are detached. * * @event * @example * const gizmo = new pc.Gizmo(camera, layer); * gizmo.on('nodes:detach', () => { * console.log('Graph nodes detached'); * }); */ Gizmo.EVENT_NODESDETACH = 'nodes:detach'; /** * Fired when when the gizmo render has updated. * * @event * @example * const gizmo = new pc.TransformGizmo(camera, layer); * gizmo.on('render:update', () => { * console.log('Gizmo render has been updated'); * }); */ Gizmo.EVENT_RENDERUPDATE = 'render:update'; export { Gizmo };