UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

310 lines (307 loc) 10 kB
import { math } from '../../core/math/math.js'; import { Quat } from '../../core/math/quat.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 { SORTMODE_NONE, PROJECTION_PERSPECTIVE } from '../../scene/constants.js'; import { Entity } from '../../framework/entity.js'; import { Layer } from '../../scene/layer.js'; const v = new Vec3(); const position = new Vec3(); const dir = new Vec3(); const rotation = new Quat(); const m1 = new Mat4(); const m2 = new Mat4(); const ray = new Ray(); const MIN_SCALE = 1e-4; const PERS_SCALE_RATIO = 0.3; const ORTHO_SCALE_RATIO = 0.32; const UPDATE_EPSILON = 1e-6; const DIST_EPSILON = 1e-4; class Gizmo extends EventHandler { static{ this.EVENT_POINTERDOWN = 'pointer:down'; } static{ this.EVENT_POINTERMOVE = 'pointer:move'; } static{ this.EVENT_POINTERUP = 'pointer:up'; } static{ this.EVENT_POSITIONUPDATE = 'position:update'; } static{ this.EVENT_ROTATIONUPDATE = 'rotation:update'; } static{ this.EVENT_SCALEUPDATE = 'scale:update'; } static{ this.EVENT_NODESATTACH = 'nodes:attach'; } static{ this.EVENT_NODESDETACH = 'nodes:detach'; } static{ this.EVENT_RENDERUPDATE = 'render:update'; } static createLayer(app, layerName = 'Gizmo', layerIndex = app.scene.layers.layerList.length) { const layer = new Layer({ name: layerName, clearDepthBuffer: true, opaqueSortMode: SORTMODE_NONE, transparentSortMode: SORTMODE_NONE }); app.scene.layers.insert(layer, layerIndex); return layer; } constructor(camera, layer, name = 'gizmo'){ super(), this._size = 1, this._scale = 1, this._coordSpace = 'world', this._handles = [], this._renderUpdate = false, this.nodes = [], this.intersectShapes = []; this._layer = layer; this._camera = camera; this._camera.layers = this._camera.layers.concat(this._layer.id); this._app = this._camera.system.app; this._device = this._app.graphicsDevice; this.root = new Entity(name); 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._handles.push(this._app.on('prerender', ()=>this.prerender())); this._handles.push(this._app.on('update', ()=>this.update())); this._handles.push(this._app.on('destroy', ()=>this.destroy())); } set enabled(state) { const cameraDist = this.root.getLocalPosition().distance(this.camera.entity.getPosition()); const enabled = state ? this.nodes.length > 0 && cameraDist > DIST_EPSILON : false; if (enabled !== this.root.enabled) { this.root.enabled = enabled; this._renderUpdate = true; } } get enabled() { return this.root.enabled; } set layer(layer) { if (this._layer === layer) { return; } this._camera.layers = this._camera.layers.filter((id)=>id !== this._layer.id); this._layer = layer; this._camera.layers = this._camera.layers.concat(this._layer.id); this.enabled = true; } get layer() { return this._layer; } set camera(camera) { if (this._camera === camera) { return; } this._camera.layers = this._camera.layers.filter((id)=>id !== this._layer.id); this._camera = camera; this._camera.layers = this._camera.layers.concat(this._layer.id); this.enabled = true; } get camera() { return this._camera; } set coordSpace(value) { this._coordSpace = value ?? this._coordSpace; this._updateRotation(); } get coordSpace() { return this._coordSpace; } set size(value) { this._size = value; this._updateScale(); } get size() { return this._size; } get facingDir() { if (this._camera.projection === PROJECTION_PERSPECTIVE) { const gizmoPos = this.root.getLocalPosition(); const cameraPos = this._camera.entity.getPosition(); return dir.sub2(cameraPos, gizmoPos).normalize(); } return dir.copy(this._camera.entity.forward).mulScalar(-1); } get cameraDir() { const cameraPos = this._camera.entity.getPosition(); const gizmoPos = this.root.getLocalPosition(); return dir.sub2(cameraPos, gizmoPos).normalize(); } _onPointerDown(e) { if (!this.enabled || document.pointerLockElement) { return; } const selection = this._getSelection(e.offsetX, e.offsetY); if (selection[0]) { e.preventDefault(); e.stopPropagation(); } const { canvas } = this._device; canvas.setPointerCapture(e.pointerId); this.fire(Gizmo.EVENT_POINTERDOWN, e.offsetX, e.offsetY, selection[0]); } _onPointerMove(e) { if (!this.enabled || document.pointerLockElement) { return; } const 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]); } _onPointerUp(e) { if (!this.enabled || document.pointerLockElement) { return; } const selection = this._getSelection(e.offsetX, e.offsetY); if (selection[0]) { e.preventDefault(); e.stopPropagation(); } const { canvas } = this._device; canvas.releasePointerCapture(e.pointerId); this.fire(Gizmo.EVENT_POINTERUP, e.offsetX, e.offsetY, selection[0]); } _updatePosition() { position.set(0, 0, 0); for(let i = 0; i < this.nodes.length; i++){ const node = this.nodes[i]; position.add(node.getPosition()); } position.mulScalar(1.0 / (this.nodes.length || 1)); if (position.equalsApprox(this.root.getLocalPosition(), UPDATE_EPSILON)) { return; } this.root.setLocalPosition(position); this.fire(Gizmo.EVENT_POSITIONUPDATE, position); this._renderUpdate = true; } _updateRotation() { rotation.set(0, 0, 0, 1); if (this._coordSpace === 'local' && this.nodes.length !== 0) { rotation.copy(this.nodes[this.nodes.length - 1].getRotation()); } if (rotation.equalsApprox(this.root.getRotation(), UPDATE_EPSILON)) { return; } this.root.setRotation(rotation); this.fire(Gizmo.EVENT_ROTATIONUPDATE, rotation.getEulerAngles()); this._renderUpdate = true; } _updateScale() { if (this._camera.projection === PROJECTION_PERSPECTIVE) { const gizmoPos = this.root.getLocalPosition(); const cameraPos = this._camera.entity.getPosition(); const 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); this._renderUpdate = true; } _getSelection(x, y) { const start = this._camera.screenToWorld(x, y, 0); const end = this._camera.screenToWorld(x, y, this._camera.farClip - this._camera.nearClip); const dir = v.copy(end).sub(start).normalize(); const selection = []; for(let i = 0; i < this.intersectShapes.length; i++){ const shape = this.intersectShapes[i]; if (shape.disabled || !shape.entity.enabled) { continue; } const parentTM = shape.entity.getWorldTransform(); for(let j = 0; j < shape.triData.length; j++){ const { tris, transform, priority } = shape.triData[j]; const triWTM = m1.copy(parentTM).mul(transform); const invTriWTM = m2.copy(triWTM).invert(); invTriWTM.transformPoint(start, ray.origin); invTriWTM.transformVector(dir, ray.direction); ray.direction.normalize(); for(let k = 0; k < tris.length; k++){ if (tris[k].intersectsRay(ray, v)) { selection.push({ dist: triWTM.transformPoint(v).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(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.enabled = true; } detach() { this.enabled = false; this.fire(Gizmo.EVENT_NODESDETACH); this.nodes = []; } prerender() {} update() { if (this._renderUpdate) { this._renderUpdate = false; this.fire(Gizmo.EVENT_RENDERUPDATE); } if (!this.enabled) { return; } this._updatePosition(); this._updateRotation(); this._updateScale(); } 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._handles.forEach((handle)=>handle.off()); this.root.destroy(); } } export { Gizmo };