playcanvas
Version:
PlayCanvas WebGL game engine
449 lines (446 loc) • 15.7 kB
JavaScript
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 };