UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

535 lines (532 loc) 17.5 kB
import { Vec3 } from '../../core/math/vec3.js'; import { Quat } from '../../core/math/quat.js'; import { TransformGizmo } from './transform-gizmo.js'; import { PlaneShape } from './shape/plane-shape.js'; import { ArrowShape } from './shape/arrow-shape.js'; import { SphereShape } from './shape/sphere-shape.js'; /** * @import { CameraComponent } from '../../framework/components/camera/component.js' * @import { GraphNode } from '../../scene/graph-node.js' * @import { Layer } from '../../scene/layer.js' * @import { GizmoAxis } from './constants.js' */ // temporary variables const v1 = new Vec3(); const v2 = new Vec3(); const point = new Vec3(); const delta = new Vec3(); const q = new Quat(); // constants const GLANCE_EPSILON = 0.01; const AXES = /** @type {('x' | 'y' | 'z')[]} */ [ 'x', 'y', 'z' ]; /** * The TranslateGizmo provides interactive 3D manipulation handles for translating/moving * {@link Entity}s in a {@link Scene}. It creates a visual widget with arrows along the X, Y * and Z axes, planes at their intersections, and a center sphere, allowing precise control over * object positioning through direct manipulation. The gizmo's visual appearance can be customized * away from the defaults as required. * * Note that the gizmo can be driven by both mouse+keyboard and touch input. * * ```javascript * // Create a layer for rendering all gizmos * const gizmoLayer = pc.Gizmo.createLayer(app); * * // Create a translate gizmo * const gizmo = new pc.TranslateGizmo(cameraComponent, gizmoLayer); * * // Create an entity to attach the gizmo to * const entity = new pc.Entity(); * entity.addComponent('render', { * type: 'box' * }); * app.root.addChild(entity); * * // Attach the gizmo to the entity * gizmo.attach([entity]); * ``` * * Relevant Engine API examples: * * - [Translate Gizmo](https://playcanvas.github.io/#/gizmos/transform-translate) * - [Editor](https://playcanvas.github.io/#/misc/editor) * * @category Gizmo */ class TranslateGizmo extends TransformGizmo { /** * Creates a new TranslateGizmo object. Use {@link Gizmo.createLayer} to create the layer * required to display the gizmo. * * @param {CameraComponent} camera - The camera component. * @param {Layer} layer - The layer responsible for rendering the gizmo. * @example * const gizmo = new pc.TranslateGizmo(camera, layer); */ constructor(camera, layer){ super(camera, layer, 'gizmo:translate'), this._shapes = { xyz: new SphereShape(this._device, { axis: 'xyz', layers: [ this._layer.id ], defaultColor: this._theme.shapeBase.xyz, hoverColor: this._theme.shapeHover.xyz, disabledColor: this._theme.disabled }), yz: new PlaneShape(this._device, { axis: 'x', layers: [ this._layer.id ], rotation: new Vec3(0, 0, -90), defaultColor: this._theme.shapeBase.x, hoverColor: this._theme.shapeHover.x, disabledColor: this._theme.disabled, depth: 1 }), xz: new PlaneShape(this._device, { axis: 'y', layers: [ this._layer.id ], rotation: new Vec3(0, 0, 0), defaultColor: this._theme.shapeBase.y, hoverColor: this._theme.shapeHover.y, disabledColor: this._theme.disabled, depth: 1 }), xy: new PlaneShape(this._device, { axis: 'z', layers: [ this._layer.id ], rotation: new Vec3(90, 0, 0), defaultColor: this._theme.shapeBase.z, hoverColor: this._theme.shapeHover.z, disabledColor: this._theme.disabled, depth: 1 }), x: new ArrowShape(this._device, { axis: 'x', layers: [ this._layer.id ], rotation: new Vec3(0, 0, -90), defaultColor: this._theme.shapeBase.x, hoverColor: this._theme.shapeHover.x, disabledColor: this._theme.disabled }), y: new ArrowShape(this._device, { axis: 'y', layers: [ this._layer.id ], rotation: new Vec3(0, 0, 0), defaultColor: this._theme.shapeBase.y, hoverColor: this._theme.shapeHover.y, disabledColor: this._theme.disabled }), z: new ArrowShape(this._device, { axis: 'z', layers: [ this._layer.id ], rotation: new Vec3(90, 0, 0), defaultColor: this._theme.shapeBase.z, hoverColor: this._theme.shapeHover.z, disabledColor: this._theme.disabled }) }, /** * Internal mapping from each attached node to their starting position in local space. * * @type {Map<GraphNode, Vec3>} * @private */ this._nodeLocalPositions = new Map(), /** * Internal mapping from each attached node to their starting position in world space. * * @type {Map<GraphNode, Vec3>} * @private */ this._nodePositions = new Map(), /** * @override */ this.snapIncrement = 1, /** * Flips the planes to face the camera. * * @type {boolean} */ this.flipPlanes = true; this._createTransform(); this.on(TransformGizmo.EVENT_TRANSFORMSTART, ()=>{ // store the initial positions of the nodes this._storeNodePositions(); // hide shapes that are not selected this._drag(true); }); this.on(TransformGizmo.EVENT_TRANSFORMMOVE, (point)=>{ // calculate translate delta and update node positions const translateDelta = delta.copy(point).sub(this._selectionStartPoint); if (this.snap) { translateDelta.mulScalar(1 / this.snapIncrement); translateDelta.round(); translateDelta.mulScalar(this.snapIncrement); } this._setNodePositions(translateDelta); }); this.on(TransformGizmo.EVENT_TRANSFORMEND, ()=>{ // show all shapes this._drag(false); }); this.on(TransformGizmo.EVENT_NODESDETACH, ()=>{ // reset stored positions this._nodeLocalPositions.clear(); this._nodePositions.clear(); }); } /** * Sets the axis gap. * * @type {number} */ set axisGap(value) { this._setArrowProp('gap', value); } /** * Gets the axis gap. * * @type {number} */ get axisGap() { return this._shapes.x.gap; } /** * Sets the axis line thickness. * * @type {number} */ set axisLineThickness(value) { this._setArrowProp('lineThickness', value); } /** * Gets the axis line thickness. * * @type {number} */ get axisLineThickness() { return this._shapes.x.lineThickness; } /** * Sets the axis line length. * * @type {number} */ set axisLineLength(value) { this._setArrowProp('lineLength', value); } /** * Gets the axis line length. * * @type {number} */ get axisLineLength() { return this._shapes.x.lineLength; } /** * Sets the axis line tolerance. * * @type {number} */ set axisLineTolerance(value) { this._setArrowProp('tolerance', value); } /** * Gets the axis line tolerance. * * @type {number} */ get axisLineTolerance() { return this._shapes.x.tolerance; } /** * Sets the arrow thickness. * * @type {number} */ set axisArrowThickness(value) { this._setArrowProp('arrowThickness', value); } /** * Gets the arrow thickness. * * @type {number} */ get axisArrowThickness() { return this._shapes.x.arrowThickness; } /** * Sets the arrow length. * * @type {number} */ set axisArrowLength(value) { this._setArrowProp('arrowLength', value); } /** * Gets the arrow length. * * @type {number} */ get axisArrowLength() { return this._shapes.x.arrowLength; } /** * Sets the plane size. * * @type {number} */ set axisPlaneSize(value) { this._setPlaneProp('size', value); } /** * Gets the plane size. * * @type {number} */ get axisPlaneSize() { return this._shapes.yz.size; } /** * Sets the plane gap. * * @type {number} */ set axisPlaneGap(value) { this._setPlaneProp('gap', value); } /** * Gets the plane gap. * * @type {number} */ get axisPlaneGap() { return this._shapes.yz.gap; } /** * Sets the axis center size. * * @type {number} */ set axisCenterSize(value) { this._shapes.xyz.radius = value; } /** * Gets the axis center size. * * @type {number} */ get axisCenterSize() { return this._shapes.xyz.radius; } /** * @type {boolean} * @deprecated Use {@link TranslateGizmo#flipPlanes} instead. * @ignore */ set flipShapes(value) { this.flipPlanes = value; } /** * @type {boolean} * @deprecated Use {@link TranslateGizmo#flipPlanes} instead. * @ignore */ get flipShapes() { return this.flipPlanes; } /** * @param {string} prop - The property to set. * @param {any} value - The value to set. * @private */ _setArrowProp(prop, value) { this._shapes.x[prop] = value; this._shapes.y[prop] = value; this._shapes.z[prop] = value; } /** * @param {string} prop - The property to set. * @param {any} value - The value to set. * @private */ _setPlaneProp(prop, value) { this._shapes.yz[prop] = value; this._shapes.xz[prop] = value; this._shapes.xy[prop] = value; } /** * @private */ _shapesLookAtCamera() { const cameraDir = this.cameraDir; // axes let changed = false; let dot, enabled; dot = cameraDir.dot(this.root.right); enabled = 1 - Math.abs(dot) > GLANCE_EPSILON; if (this._shapes.x.entity.enabled !== enabled) { this._shapes.x.entity.enabled = enabled; changed = true; } dot = cameraDir.dot(this.root.up); enabled = 1 - Math.abs(dot) > GLANCE_EPSILON; if (this._shapes.y.entity.enabled !== enabled) { this._shapes.y.entity.enabled = enabled; changed = true; } dot = cameraDir.dot(this.root.forward); enabled = 1 - Math.abs(dot) > GLANCE_EPSILON; if (this._shapes.z.entity.enabled !== enabled) { this._shapes.z.entity.enabled = enabled; changed = true; } // planes let flipped; v1.cross(cameraDir, this.root.right); enabled = 1 - v1.length() > GLANCE_EPSILON; if (this._shapes.yz.entity.enabled !== enabled) { this._shapes.yz.entity.enabled = enabled; changed = true; } flipped = this.flipPlanes ? v2.set(0, +(v1.dot(this.root.forward) < 0), +(v1.dot(this.root.up) < 0)) : Vec3.ZERO; if (!this._shapes.yz.flipped.equals(flipped)) { this._shapes.yz.flipped = flipped; changed = true; } v1.cross(cameraDir, this.root.forward); enabled = 1 - v1.length() > GLANCE_EPSILON; if (this._shapes.xy.entity.enabled !== enabled) { this._shapes.xy.entity.enabled = enabled; changed = true; } flipped = this.flipPlanes ? v2.set(+(v1.dot(this.root.up) < 0), +(v1.dot(this.root.right) > 0), 0) : Vec3.ZERO; if (!this._shapes.xy.flipped.equals(flipped)) { this._shapes.xy.flipped = flipped; changed = true; } v1.cross(cameraDir, this.root.up); enabled = 1 - v1.length() > GLANCE_EPSILON; if (this._shapes.xz.entity.enabled !== enabled) { this._shapes.xz.entity.enabled = enabled; changed = true; } flipped = this.flipPlanes ? v2.set(+(v1.dot(this.root.forward) > 0), 0, +(v1.dot(this.root.right) > 0)) : Vec3.ZERO; if (!this._shapes.xz.flipped.equals(flipped)) { this._shapes.xz.flipped = flipped; changed = true; } if (changed) { this._renderUpdate = true; } } /** * @param {boolean} state - The state. * @private */ _drag(state) { for(const axis in this._shapes){ const shape = this._shapes[axis]; switch(this.dragMode){ case 'show': { continue; } case 'hide': { shape.visible = !state; continue; } case 'selected': { if (this._selectedAxis === 'xyz') { shape.visible = state ? axis.length === 1 : true; continue; } if (this._selectedIsPlane) { shape.visible = state ? axis.length === 1 && !axis.includes(this._selectedAxis) : true; continue; } shape.visible = state ? axis === this._selectedAxis : true; } } } this._renderUpdate = true; } /** * @private */ _storeNodePositions() { for(let i = 0; i < this.nodes.length; i++){ const node = this.nodes[i]; this._nodeLocalPositions.set(node, node.getLocalPosition().clone()); this._nodePositions.set(node, node.getPosition().clone()); } } /** * @param {Vec3} translateDelta - The delta to apply to the node positions. * @private */ _setNodePositions(translateDelta) { for(let i = 0; i < this.nodes.length; i++){ const node = this.nodes[i]; if (this._coordSpace === 'local') { const pos = this._nodeLocalPositions.get(node); if (!pos) { continue; } v1.copy(translateDelta); node.parent?.getWorldTransform().getScale(v2); v2.x = 1 / v2.x; v2.y = 1 / v2.y; v2.z = 1 / v2.z; q.copy(node.getLocalRotation()).transformVector(v1, v1); v1.mul(v2); node.setLocalPosition(v1.add(pos)); } else { const pos = this._nodePositions.get(node); if (!pos) { continue; } node.setPosition(v1.copy(translateDelta).add(pos)); } } this._updatePosition(); } /** * @param {number} x - The x coordinate. * @param {number} y - The y coordinate. * @returns {Vec3} The point (space is {@link TransformGizmo#coordSpace}). * @protected */ _screenToPoint(x, y) { const mouseWPos = this._camera.screenToWorld(x, y, 1); const axis = this._selectedAxis; const isPlane = this._selectedIsPlane; const ray = this._createRay(mouseWPos); const plane = this._createPlane(axis, axis === 'xyz', !isPlane); if (!plane.intersectsRay(ray, point)) { return point; } // rotate point back to world coords q.copy(this._rootStartRot).invert().transformVector(point, point); // project point onto axis if (!isPlane && axis !== 'xyz') { this._projectToAxis(point, axis); } return point; } /** * @param {Vec3} pos - The position. * @param {Quat} rot - The rotation. * @param {GizmoAxis} activeAxis - The active axis. * @param {boolean} activeIsPlane - Whether the active axis is a plane. * @override */ _drawGuideLines(pos, rot, activeAxis, activeIsPlane) { for (const axis of AXES){ if (this._dragging || activeAxis === 'xyz') { this._drawSpanLine(pos, rot, axis); continue; } if (activeIsPlane) { if (axis !== activeAxis) { this._drawSpanLine(pos, rot, axis); } } else { if (axis === activeAxis) { this._drawSpanLine(pos, rot, axis); } } } } /** * @override */ prerender() { super.prerender(); if (!this.enabled) { return; } this._shapesLookAtCamera(); } } export { TranslateGizmo };