playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
643 lines (642 loc) • 18.7 kB
JavaScript
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
};