playcanvas
Version:
PlayCanvas WebGL game engine
590 lines (587 loc) • 19.2 kB
JavaScript
import { math } from '../../core/math/math.js';
import { Color } from '../../core/math/color.js';
import { Quat } from '../../core/math/quat.js';
import { Vec3 } from '../../core/math/vec3.js';
import { Ray } from '../../core/shape/ray.js';
import { Plane } from '../../core/shape/plane.js';
import { PROJECTION_PERSPECTIVE } from '../../scene/constants.js';
import { color4from3, color3from4, COLOR_GRAY, COLOR_YELLOW, COLOR_BLUE, COLOR_GREEN, COLOR_RED } from './color.js';
import { GIZMOAXIS_X, GIZMOAXIS_Y, GIZMOAXIS_Z, GIZMOAXIS_FACE, GIZMOAXIS_XYZ } from './constants.js';
import { Gizmo } from './gizmo.js';
/**
* @import { Shape } from './shape/shape.js'
* @import { CameraComponent } from '../../framework/components/camera/component.js'
* @import { Layer } from '../../scene/layer.js'
* @import { MeshInstance } from '../../scene/mesh-instance.js'
*/ // temporary variables
var tmpV1 = new Vec3();
var tmpV2 = new Vec3();
var tmpQ1 = new Quat();
var tmpR1 = new Ray();
var tmpP1 = new Plane();
// constants
var VEC3_AXES = Object.keys(tmpV1);
/**
* The base class for all transform gizmos.
*
* @category Gizmo
*/ class TransformGizmo extends Gizmo {
/**
* Sets whether shading are enabled. Defaults to true.
*
* @type {boolean}
*/ set shading(value) {
this._shading = this.root.enabled && value;
for(var name in this._shapes){
this._shapes[name].shading = this._shading;
}
}
/**
* Gets whether shading are enabled. Defaults to true.
*
* @type {boolean}
*/ get shading() {
return this._shading;
}
/**
* Sets whether snapping is enabled. Defaults to false.
*
* @type {boolean}
*/ set snap(value) {
this._snap = this.root.enabled && value;
}
/**
* Gets whether snapping is enabled. Defaults to false.
*
* @type {boolean}
*/ get snap() {
return this._snap;
}
/**
* Sets the X axis color.
*
* @type {Color}
*/ set xAxisColor(value) {
this._updateAxisColor(GIZMOAXIS_X, value);
}
/**
* Gets the X axis color.
*
* @type {Color}
*/ get xAxisColor() {
return this._meshColors.axis.x;
}
/**
* Sets the Y axis color.
*
* @type {Color}
*/ set yAxisColor(value) {
this._updateAxisColor(GIZMOAXIS_Y, value);
}
/**
* Gets the Y axis color.
*
* @type {Color}
*/ get yAxisColor() {
return this._meshColors.axis.y;
}
/**
* Sets the Z axis color.
*
* @type {Color}
*/ set zAxisColor(value) {
this._updateAxisColor(GIZMOAXIS_Z, value);
}
/**
* Gets the Z axis color.
*
* @type {Color}
*/ get zAxisColor() {
return this._meshColors.axis.z;
}
/**
* Sets the color alpha for all axes.
*
* @type {number}
*/ set colorAlpha(value) {
this._colorAlpha = math.clamp(value, 0, 1);
this._meshColors.axis.x.copy(this._colorSemi(this._meshColors.axis.x));
this._meshColors.axis.y.copy(this._colorSemi(this._meshColors.axis.y));
this._meshColors.axis.z.copy(this._colorSemi(this._meshColors.axis.z));
this._meshColors.axis.xyz.copy(this._colorSemi(this._meshColors.axis.xyz));
this._meshColors.axis.f.copy(this._colorSemi(this._meshColors.axis.f));
for(var name in this._shapes){
this._shapes[name].hover(!!this._hoverAxis);
}
}
/**
* Gets the color alpha for all axes.
*
* @type {number}
*/ get colorAlpha() {
return this._colorAlpha;
}
/**
* @param {Color} color - The color to set.
* @returns {Color} - The color with alpha applied.
* @private
*/ _colorSemi(color) {
return color4from3(color, this._colorAlpha);
}
/**
* @param {string} axis - The axis to update.
* @param {any} value - The value to set.
* @private
*/ _updateAxisColor(axis, value) {
var color3 = color3from4(value);
var color4 = this._colorSemi(value);
this._guideColors[axis].copy(color3);
this._meshColors.axis[axis].copy(color4);
this._meshColors.hover[axis].copy(color3);
for(var name in this._shapes){
this._shapes[name].hover(!!this._hoverAxis);
}
}
/**
* @param {MeshInstance} [meshInstance] - The mesh instance.
* @returns {string} - The axis.
* @private
*/ _getAxis(meshInstance) {
if (!meshInstance) {
return '';
}
return meshInstance.node.name.split(':')[1];
}
/**
* @param {MeshInstance} [meshInstance] - The mesh instance.
* @returns {boolean} - Whether the mesh instance is a plane.
* @private
*/ _getIsPlane(meshInstance) {
if (!meshInstance) {
return false;
}
return meshInstance.node.name.indexOf('plane') !== -1;
}
/**
* @param {MeshInstance} [meshInstance] - The mesh instance.
* @private
*/ _hover(meshInstance) {
if (this._dragging) {
return;
}
this._hoverAxis = this._getAxis(meshInstance);
this._hoverIsPlane = this._getIsPlane(meshInstance);
var _this__shapeMap_get;
var shape = meshInstance ? (_this__shapeMap_get = this._shapeMap.get(meshInstance)) != null ? _this__shapeMap_get : null : null;
if (shape === this._hoverShape) {
return;
}
if (this._hoverShape) {
this._hoverShape.hover(false);
this._hoverShape = null;
}
if (shape) {
shape.hover(true);
this._hoverShape = shape;
}
this.fire(Gizmo.EVENT_RENDERUPDATE);
}
/**
* @param {Vec3} mouseWPos - The mouse world position.
* @returns {Ray} - The ray.
* @protected
*/ _createRay(mouseWPos) {
if (this._camera.projection === PROJECTION_PERSPECTIVE) {
tmpR1.origin.copy(this._camera.entity.getPosition());
tmpR1.direction.sub2(mouseWPos, tmpR1.origin).normalize();
return tmpR1;
}
var orthoDepth = this._camera.farClip - this._camera.nearClip;
tmpR1.origin.sub2(mouseWPos, tmpV1.copy(this._camera.entity.forward).mulScalar(orthoDepth));
tmpR1.direction.copy(this._camera.entity.forward);
return tmpR1;
}
/**
* @param {string} axis - The axis to create the plane for.
* @param {boolean} isFacing - Whether the axis is facing the camera.
* @param {boolean} isLine - Whether the axis is a line.
* @returns {Plane} - The plane.
* @protected
*/ _createPlane(axis, isFacing, isLine) {
var facingDir = tmpV1.copy(this.facing);
var normal = tmpP1.normal.set(0, 0, 0);
if (isFacing) {
// all axes so set normal to plane facing camera
normal.copy(facingDir);
} else {
// set plane normal based on axis
normal[axis] = 1;
this._rootStartRot.transformVector(normal, normal);
if (isLine) {
// set plane normal to face camera but keep normal perpendicular to axis
tmpV2.cross(normal, facingDir).normalize();
normal.cross(tmpV2, normal).normalize();
}
}
return tmpP1.setFromPointNormal(this._rootStartPos, normal);
}
/**
* @param {string} axis - The axis
* @param {Vec3} dir - The direction
* @returns {Vec3} - The direction
* @protected
*/ _dirFromAxis(axis, dir) {
if (axis === GIZMOAXIS_FACE) {
dir.copy(this._camera.entity.forward).mulScalar(-1);
} else {
dir.set(0, 0, 0);
dir[axis] = 1;
}
return dir;
}
/**
* @param {Vec3} point - The point to project.
* @param {string} axis - The axis to project to.
* @protected
*/ _projectToAxis(point, axis) {
// set normal to axis and project position from plane onto normal
tmpV1.set(0, 0, 0);
tmpV1[axis] = 1;
point.copy(tmpV1.mulScalar(tmpV1.dot(point)));
// set other axes to zero (floating point fix)
var v = point[axis];
point.set(0, 0, 0);
point[axis] = v;
}
/**
* @param {number} x - The x coordinate.
* @param {number} y - The y coordinate.
* @param {boolean} isFacing - Whether the axis is facing the camera.
* @param {boolean} isLine - Whether the axis is a line.
* @returns {Vec3} - The point.
* @protected
*/ _screenToPoint(x, y, isFacing, isLine) {
if (isFacing === void 0) isFacing = false;
if (isLine === void 0) isLine = false;
var mouseWPos = this._camera.screenToWorld(x, y, 1);
var axis = this._selectedAxis;
var ray = this._createRay(mouseWPos);
var plane = this._createPlane(axis, isFacing, isLine);
var point = new Vec3();
plane.intersectsRay(ray, point);
return point;
}
/**
* @private
*/ _drawGuideLines() {
var gizmoPos = this.root.getPosition();
var gizmoRot = tmpQ1.copy(this.root.getRotation());
var checkAxis = this._hoverAxis || this._selectedAxis;
var checkIsPlane = this._hoverIsPlane || this._selectedIsPlane;
for(var i = 0; i < VEC3_AXES.length; i++){
var axis = VEC3_AXES[i];
if (checkAxis === GIZMOAXIS_XYZ) {
this._drawSpanLine(gizmoPos, gizmoRot, axis);
continue;
}
if (checkIsPlane) {
if (axis !== checkAxis) {
this._drawSpanLine(gizmoPos, gizmoRot, axis);
}
} else {
if (axis === checkAxis) {
this._drawSpanLine(gizmoPos, gizmoRot, axis);
}
}
}
}
/**
* @param {Vec3} pos - The position.
* @param {Quat} rot - The rotation.
* @param {string} axis - The axis.
* @private
*/ _drawSpanLine(pos, rot, axis) {
tmpV1.set(0, 0, 0);
tmpV1[axis] = 1;
tmpV1.mulScalar(this._camera.farClip - this._camera.nearClip);
tmpV2.copy(tmpV1).mulScalar(-1);
rot.transformVector(tmpV1, tmpV1);
rot.transformVector(tmpV2, tmpV2);
this._app.drawLine(tmpV1.add(pos), tmpV2.add(pos), this._guideColors[axis], true);
}
/**
* @protected
*/ _createTransform() {
// shapes
for(var key in this._shapes){
var shape = this._shapes[key];
this.root.addChild(shape.entity);
this.intersectShapes.push(shape);
for(var i = 0; i < shape.meshInstances.length; i++){
this._shapeMap.set(shape.meshInstances[i], shape);
}
}
}
/**
* Set the shape to be enabled or disabled.
*
* @param {string} shapeAxis - The shape axis. Can be:
*
* - {@link GIZMOAXIS_X}
* - {@link GIZMOAXIS_Y}
* - {@link GIZMOAXIS_Z}
* - {@link GIZMOAXIS_YZ}
* - {@link GIZMOAXIS_XZ}
* - {@link GIZMOAXIS_XY}
* - {@link GIZMOAXIS_XYZ}
* - {@link GIZMOAXIS_FACE}
*
* @param {boolean} enabled - The enabled state of shape.
*/ enableShape(shapeAxis, enabled) {
if (!this._shapes.hasOwnProperty(shapeAxis)) {
return;
}
this._shapes[shapeAxis].disabled = !enabled;
}
/**
* Get the enabled state of the shape.
*
* @param {string} shapeAxis - The shape axis. Can be:
*
* - {@link GIZMOAXIS_X}
* - {@link GIZMOAXIS_Y}
* - {@link GIZMOAXIS_Z}
* - {@link GIZMOAXIS_YZ}
* - {@link GIZMOAXIS_XZ}
* - {@link GIZMOAXIS_XY}
* - {@link GIZMOAXIS_XYZ}
* - {@link GIZMOAXIS_FACE}
*
* @returns {boolean} - Then enabled state of the shape
*/ isShapeEnabled(shapeAxis) {
if (!this._shapes.hasOwnProperty(shapeAxis)) {
return false;
}
return !this._shapes[shapeAxis].disabled;
}
/**
* @override
*/ destroy() {
super.destroy();
for(var key in this._shapes){
this._shapes[key].destroy();
}
}
/**
* Creates a new TransformGizmo object.
*
* @param {CameraComponent} camera - The camera component.
* @param {Layer} layer - The render layer.
* @example
* const gizmo = new pc.TransformGizmo(camera, layer);
*/ constructor(camera, layer){
super(camera, layer), /**
* Internal color alpha value.
*
* @type {number}
* @private
*/ this._colorAlpha = 0.6, /**
* Internal color for meshes.
*
* @type {{ axis: Record<string, Color>, hover: Record<string, Color>, disabled: Color }}
* @protected
*/ this._meshColors = {
axis: {
x: this._colorSemi(COLOR_RED),
y: this._colorSemi(COLOR_GREEN),
z: this._colorSemi(COLOR_BLUE),
xyz: this._colorSemi(Color.WHITE),
f: this._colorSemi(Color.WHITE)
},
hover: {
x: COLOR_RED.clone(),
y: COLOR_GREEN.clone(),
z: COLOR_BLUE.clone(),
xyz: Color.WHITE.clone(),
f: COLOR_YELLOW.clone()
},
disabled: COLOR_GRAY.clone()
}, /**
* Internal version of the guide line color.
*
* @type {Record<string, Color>}
* @protected
*/ this._guideColors = {
x: COLOR_RED.clone(),
y: COLOR_GREEN.clone(),
z: COLOR_BLUE.clone(),
f: COLOR_YELLOW.clone()
}, /**
* Internal gizmo starting rotation in world space.
*
* @type {Vec3}
* @protected
*/ this._rootStartPos = new Vec3(), /**
* Internal gizmo starting rotation in world space.
*
* @type {Quat}
* @protected
*/ this._rootStartRot = new Quat(), /**
* Internal state of if shading is enabled. Defaults to true.
*
* @type {boolean}
* @protected
*/ this._shading = false, /**
* Internal object containing the gizmo shapes to render.
*
* @type {Object.<string, Shape>}
* @protected
*/ this._shapes = {}, /**
* Internal mapping of mesh instances to gizmo shapes.
*
* @type {Map<MeshInstance, Shape>}
* @private
*/ this._shapeMap = new Map(), /**
* Internal currently hovered shape.
*
* @type {Shape | null}
* @private
*/ this._hoverShape = null, /**
* Internal currently hovered axis.
*
* @type {string}
* @private
*/ this._hoverAxis = '', /**
* Internal state of if currently hovered shape is a plane.
*
* @type {boolean}
* @private
*/ this._hoverIsPlane = false, /**
* Internal state of if there is no selection.
*
* @type {boolean}
* @private
*/ this._noSelection = false, /**
* Internal currently selected axis.
*
* @type {string}
* @protected
*/ this._selectedAxis = '', /**
* Internal state of if currently selected shape is a plane.
*
* @type {boolean}
* @protected
*/ this._selectedIsPlane = false, /**
* Internal selection starting coordinates in world space.
*
* @type {Vec3}
* @protected
*/ this._selectionStartPoint = new Vec3(), /**
* Internal state for if the gizmo is being dragged.
*
* @type {boolean}
* @protected
*/ this._dragging = false, /**
* Internal state for if snapping is enabled. Defaults to false.
*
* @type {boolean}
* @private
*/ this._snap = false, /**
* Snapping increment. Defaults to 1.
*
* @type {number}
*/ this.snapIncrement = 1;
this._app.on('prerender', ()=>{
if (!this.root.enabled) {
return;
}
this._drawGuideLines();
});
this.on(Gizmo.EVENT_POINTERDOWN, (x, y, meshInstance)=>{
var shape = this._shapeMap.get(meshInstance);
if (shape == null ? void 0 : shape.disabled) {
return;
}
if (this._dragging) {
return;
}
if (!meshInstance) {
this._noSelection = true;
return;
}
this._selectedAxis = this._getAxis(meshInstance);
this._selectedIsPlane = this._getIsPlane(meshInstance);
this._rootStartPos.copy(this.root.getPosition());
this._rootStartRot.copy(this.root.getRotation());
var point = this._screenToPoint(x, y);
this._selectionStartPoint.copy(point);
this._dragging = true;
this.fire(TransformGizmo.EVENT_TRANSFORMSTART, point, x, y);
});
this.on(Gizmo.EVENT_POINTERMOVE, (x, y, meshInstance)=>{
var shape = this._shapeMap.get(meshInstance);
if (shape == null ? void 0 : shape.disabled) {
return;
}
if (!this._noSelection) {
this._hover(meshInstance);
}
if (!this._dragging) {
return;
}
var point = this._screenToPoint(x, y);
this.fire(TransformGizmo.EVENT_TRANSFORMMOVE, point, x, y);
this._hoverAxis = '';
this._hoverIsPlane = false;
});
this.on(Gizmo.EVENT_POINTERUP, (x, y, meshInstance)=>{
this._noSelection = false;
this._hover(meshInstance);
if (!this._dragging) {
return;
}
this._dragging = false;
this.fire(TransformGizmo.EVENT_TRANSFORMEND);
this._selectedAxis = '';
this._selectedIsPlane = false;
});
this.on(Gizmo.EVENT_NODESDETACH, ()=>{
this.snap = false;
this._hoverAxis = '';
this._hoverIsPlane = false;
this._hover();
this.fire(Gizmo.EVENT_POINTERUP);
});
}
}
/**
* Fired when when the transformation has started.
*
* @event
* @example
* const gizmo = new pc.TransformGizmo(camera, layer);
* gizmo.on('transform:start', () => {
* console.log('Transformation started');
* });
*/ TransformGizmo.EVENT_TRANSFORMSTART = 'transform:start';
/**
* Fired during the transformation.
*
* @event
* @example
* const gizmo = new pc.TransformGizmo(camera, layer);
* gizmo.on('transform:move', (pointDelta, angleDelta) => {
* console.log('Transformation moved by ${pointDelta} (angle: ${angleDelta})');
* });
*/ TransformGizmo.EVENT_TRANSFORMMOVE = 'transform:move';
/**
* Fired when when the transformation has ended.
*
* @event
* @example
* const gizmo = new pc.TransformGizmo(camera, layer);
* gizmo.on('transform:end', () => {
* console.log('Transformation ended');
* });
*/ TransformGizmo.EVENT_TRANSFORMEND = 'transform:end';
export { TransformGizmo };