UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

643 lines (642 loc) 18.7 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); 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 { PROJECTION_PERSPECTIVE, SORTMODE_NONE } 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; const _Gizmo = class _Gizmo extends EventHandler { /** * 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. * @param {string} [name] - The name of the gizmo. Defaults to 'gizmo'. * @example * const gizmo = new pc.Gizmo(camera, layer); */ constructor(camera, layer, name = "gizmo") { super(); /** * Internal version of the gizmo size. Defaults to 1. * * @private */ __publicField(this, "_size", 1); /** * Internal version of the gizmo scale. Defaults to 1. * * @protected */ __publicField(this, "_scale", 1); /** * Internal version of coordinate space. Defaults to 'world'. * * @type {GizmoSpace} * @protected */ __publicField(this, "_coordSpace", "world"); /** * Internal reference to the app containing the gizmo. * * @type {AppBase} * @protected */ __publicField(this, "_app"); /** * Internal reference to the graphics device of the app. * * @type {GraphicsDevice} * @protected */ __publicField(this, "_device"); /** * Internal list of app event handles for the gizmo. * * @type {EventHandle[]} * @protected */ __publicField(this, "_handles", []); /** * Internal array of mouse buttons that can interact with the gizmo. * * @type {[boolean, boolean, boolean]} * @protected */ __publicField(this, "_mouseButtons", [true, true, true]); /** * Internal reference to camera component to view the gizmo. * * @type {CameraComponent} * @protected */ __publicField(this, "_camera"); /** * Internal reference to layer to render the gizmo.. * * @type {Layer} * @protected */ __publicField(this, "_layer"); /** * Internal flag to track if a render update is required. * * @protected */ __publicField(this, "_renderUpdate", false); /** * The graph nodes attached to the gizmo. * * @type {GraphNode[]} */ __publicField(this, "nodes", []); /** * The root gizmo entity. * * @type {Entity} */ __publicField(this, "root"); /** * The intersection shapes for the gizmo. * * @type {Shape[]} */ __publicField(this, "intersectShapes", []); /** * Flag to indicate whether to call `preventDefault` on pointer events. */ __publicField(this, "preventDefault", true); 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())); } /** * 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 = "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; } /** * Sets the gizmo enabled state. * * @type {boolean} */ 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; } } /** * Gets the gizmo enabled state. * * @type {boolean} */ get enabled() { return this.root.enabled; } /** * Array of mouse buttons that can interact with the gizmo. The button indices are defined as: * * - 0: Left button * - 1: Middle button * - 2: Right button * * The full list of button indices can be found here: * {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button} * * @type {[boolean, boolean, boolean]} */ get mouseButtons() { return this._mouseButtons; } /** * Sets the gizmo render layer. * * @param {Layer} layer - The layer to render the gizmo. */ 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; } /** * Gets the gizmo render layer. * * @type {Layer} */ get layer() { return this._layer; } /** * Sets the camera component to view the gizmo. * * @type {CameraComponent} camera - The camera component. */ 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; } /** * Gets the camera component to view the gizmo. * * @type {CameraComponent} The camera component. */ get camera() { return this._camera; } /** * Sets the gizmo coordinate space. Defaults to 'world' * * @type {GizmoSpace} */ set coordSpace(value) { this._coordSpace = value ?? this._coordSpace; this._updateRotation(); } /** * Gets the gizmo coordinate space. * * @type {GizmoSpace} */ 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 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); } /** * @type {Vec3} * @protected */ get cameraDir() { const cameraPos = this._camera.entity.getPosition(); const gizmoPos = this.root.getLocalPosition(); return dir.sub2(cameraPos, gizmoPos).normalize(); } /** * @param {PointerEvent} e - The pointer event. * @private */ _onPointerDown(e) { if (!this.enabled || document.pointerLockElement) { return; } if (!this.mouseButtons[e.button]) { return; } const selection = this._getSelection(e.offsetX, e.offsetY); if (selection[0]) { if (this.preventDefault) { e.preventDefault(); } e.stopPropagation(); } const { 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.enabled || document.pointerLockElement) { return; } const selection = this._getSelection(e.offsetX, e.offsetY); if (selection[0]) { if (this.preventDefault) { 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.enabled || document.pointerLockElement) { return; } if (!this.mouseButtons[e.button]) { return; } const selection = this._getSelection(e.offsetX, e.offsetY); if (selection[0]) { if (this.preventDefault) { e.preventDefault(); } e.stopPropagation(); } const { canvas } = this._device; canvas.releasePointerCapture(e.pointerId); this.fire(_Gizmo.EVENT_POINTERUP, e.offsetX, e.offsetY, selection[0]); } /** @protected */ _updatePosition() { position.set(0, 0, 0); if (this._coordSpace === "local") { position.copy(this.nodes[this.nodes.length - 1].getPosition()); } else { for (let i = 0; i < this.nodes.length; i++) { const node = this.nodes[i]; position.add(node.getPosition()); } position.mulScalar(1 / (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; } /** @protected */ _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; } /** @protected */ _updateScale() { if (this._camera.projection === PROJECTION_PERSPECTIVE) { const gizmoPos = this.root.getLocalPosition(); const cameraPos = this._camera.entity.getPosition(); const dist = v.sub2(gizmoPos, cameraPos).dot(this._camera.entity.forward); 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; } /** * @param {number} x - The x coordinate. * @param {number} y - The y coordinate. * @returns {MeshInstance[]} - The mesh instances. * @private */ _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 dir2 = 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(dir2, 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 }); } } } } 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 (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; } /** * Detaches all graph nodes from the gizmo. * * @example * const gizmo = new pc.Gizmo(camera, layer); * gizmo.attach([boxA, boxB]); * gizmo.detach(); */ detach() { this.enabled = false; this.fire(_Gizmo.EVENT_NODESDETACH); this.nodes = []; } /** * Pre-render method. This is called before the gizmo is rendered. * * @example * const gizmo = new pc.Gizmo(camera, layer); * gizmo.attach([boxA, boxB]); * gizmo.prerender(); */ prerender() { } /** * Updates the gizmo position, rotation, and scale. * * @example * const gizmo = new pc.Gizmo(camera, layer); * gizmo.attach([boxA, boxB]); * gizmo.update(); */ update() { if (this._renderUpdate) { this._renderUpdate = false; this.fire(_Gizmo.EVENT_RENDERUPDATE); } if (!this.enabled) { return; } this._updatePosition(); this._updateRotation(); this._updateScale(); } /** * 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._handles.forEach((handle) => handle.off()); this.root.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}`); * }); */ __publicField(_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}`); * }); */ __publicField(_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}`); * }) */ __publicField(_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}`); * }) */ __publicField(_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}`); * }); */ __publicField(_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}`); * }); */ __publicField(_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'); * }); */ __publicField(_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'); * }); */ __publicField(_Gizmo, "EVENT_NODESDETACH", "nodes:detach"); /** * Fired 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'); * }); */ __publicField(_Gizmo, "EVENT_RENDERUPDATE", "render:update"); let Gizmo = _Gizmo; export { Gizmo };