UNPKG

playcanvas

Version:

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

673 lines (672 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 { 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 { COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_GRAY, color4from3 } from "./color.js"; import { Gizmo } from "./gizmo.js"; import { Debug } from "../../core/debug.js"; const v1 = new Vec3(); const v2 = new Vec3(); const point = new Vec3(); const ray = new Ray(); const plane = new Plane(); const color = new Color(); const AXES = ( /** @type {('x' | 'y' | 'z')[]} */ ["x", "y", "z"] ); const _TransformGizmo = class _TransformGizmo extends Gizmo { /** * Creates a new TransformGizmo object. * * @param {CameraComponent} camera - The camera component. * @param {Layer} layer - The render layer. * @param {string} [name] - The name of the gizmo. * @example * const gizmo = new pc.TransformGizmo(camera, layer); */ constructor(camera, layer, name = "gizmo:transform") { super(camera, layer, name); /** * Internal theme. * * @type {GizmoTheme} * @protected */ __publicField(this, "_theme", { shapeBase: { x: COLOR_RED.clone(), y: COLOR_GREEN.clone(), z: COLOR_BLUE.clone(), xyz: new Color(0.8, 0.8, 0.8, 1), f: new Color(0.8, 0.8, 0.8, 1) }, shapeHover: { x: new Color().lerp(COLOR_RED, Color.WHITE, 0.75), y: new Color().lerp(COLOR_GREEN, Color.WHITE, 0.75), z: new Color().lerp(COLOR_BLUE, Color.WHITE, 0.75), xyz: Color.WHITE.clone(), f: Color.WHITE.clone() }, guideBase: { x: COLOR_RED.clone(), y: COLOR_GREEN.clone(), z: COLOR_BLUE.clone() }, guideOcclusion: 0.8, disabled: COLOR_GRAY.clone() }); /** * Internal gizmo starting rotation in world space. * * @protected */ __publicField(this, "_rootStartPos", new Vec3()); /** * Internal gizmo starting rotation in world space. * * @protected */ __publicField(this, "_rootStartRot", new Quat()); /** * Internal object containing the gizmo shapes to render. * * @type {{ [key in GizmoAxis]?: Shape }} * @protected */ __publicField(this, "_shapes", {}); /** * Internal mapping of mesh instances to gizmo shapes. * * @type {Map<MeshInstance, Shape>} * @private */ __publicField(this, "_shapeMap", /* @__PURE__ */ new Map()); /** * Internal currently hovered axes * * @type {Set<GizmoAxis>} * @private */ __publicField(this, "_hovering", /* @__PURE__ */ new Set()); /** * Internal currently hovered axis. * * @type {GizmoAxis | ''} * @private */ __publicField(this, "_hoverAxis", ""); /** * Internal state of if currently hovered shape is a plane. * * @private */ __publicField(this, "_hoverIsPlane", false); /** * Internal currently selected axis. * * @type {GizmoAxis | ''} * @protected */ __publicField(this, "_selectedAxis", ""); /** * Internal state of if currently selected shape is a plane. * * @protected */ __publicField(this, "_selectedIsPlane", false); /** * Internal selection starting coordinates in world space. * * @protected */ __publicField(this, "_selectionStartPoint", new Vec3()); /** * Whether snapping is enabled. Defaults to false. */ __publicField(this, "snap", false); /** * Snapping increment. Defaults to 1. */ __publicField(this, "snapIncrement", 1); /** * Whether to hide the shapes when dragging. Defaults to 'selected'. * * @type {GizmoDragMode} */ __publicField(this, "dragMode", "selected"); this.on(Gizmo.EVENT_POINTERDOWN, (x, y, meshInstance) => { const shape = this._shapeMap.get(meshInstance); if (shape?.disabled) { return; } if (this._dragging) { return; } if (!meshInstance) { return; } this._hoverAxis = ""; this._hoverIsPlane = false; this._selectedAxis = this._getAxis(meshInstance); this._selectedIsPlane = this._getIsPlane(meshInstance); this._rootStartPos.copy(this.root.getLocalPosition()); this._rootStartRot.copy(this.root.getRotation()); const point2 = this._screenToPoint(x, y); this._selectionStartPoint.copy(point2); this.fire(_TransformGizmo.EVENT_TRANSFORMSTART, point2, x, y); }); this.on(Gizmo.EVENT_POINTERMOVE, (x, y, meshInstance) => { const shape = this._shapeMap.get(meshInstance); if (shape?.disabled) { return; } this._hover(meshInstance); if (!this._dragging) { return; } const point2 = this._screenToPoint(x, y); this.fire(_TransformGizmo.EVENT_TRANSFORMMOVE, point2, x, y); }); this.on(Gizmo.EVENT_POINTERUP, (_x, _y, meshInstance) => { this._hover(meshInstance); if (!this._dragging) { return; } if (meshInstance) { this._hoverAxis = this._selectedAxis; this._hoverIsPlane = this._selectedIsPlane; } this._selectedAxis = ""; this._selectedIsPlane = false; this.fire(_TransformGizmo.EVENT_TRANSFORMEND); }); this.on(Gizmo.EVENT_NODESDETACH, () => { this._hoverAxis = ""; this._hoverIsPlane = false; this._hover(); this.fire(Gizmo.EVENT_POINTERUP); }); } /** * Gets the current theme for the gizmo. * * @type {GizmoTheme} */ get theme() { return this._theme; } /** * @type {Color} * @deprecated Use {@link setTheme} instead. * @ignore */ set xAxisColor(value) { this.setTheme({ shapeBase: { x: value }, shapeHover: { x: color4from3(value, this.colorAlpha) }, guideBase: { x: value }, guideOcclusion: 1 }); } /** * @type {Color} * @deprecated Use {@link theme} instead. * @ignore */ get xAxisColor() { return this._theme.shapeBase.x; } /** * @type {Color} * @deprecated Use {@link setTheme} instead. * @ignore */ set yAxisColor(value) { this.setTheme({ shapeBase: { y: value }, shapeHover: { y: color4from3(value, this.colorAlpha) }, guideBase: { y: value }, guideOcclusion: 1 }); } /** * @type {Color} * @deprecated Use {@link theme} instead. * @ignore */ get yAxisColor() { return this._theme.shapeBase.y; } /** * @type {Color} * @deprecated Use {@link setTheme} instead. * @ignore */ set zAxisColor(value) { this.setTheme({ shapeBase: { z: value }, shapeHover: { z: color4from3(value, this.colorAlpha) }, guideBase: { z: value }, guideOcclusion: 1 }); } /** * @type {Color} * @deprecated Use {@link theme} instead. * @ignore */ get zAxisColor() { return this._theme.shapeBase.z; } /** * @type {number} * @deprecated Use {@link setTheme} instead. * @ignore */ set colorAlpha(value) { this.setTheme({ shapeHover: { x: color4from3(this._theme.shapeHover.x, value), y: color4from3(this._theme.shapeHover.y, value), z: color4from3(this._theme.shapeHover.z, value), xyz: color4from3(this._theme.shapeHover.xyz, value), f: color4from3(this._theme.shapeHover.f, value) } }); } /** * @type {number} * @deprecated Use {@link theme} instead. * @ignore */ get colorAlpha() { return (this._theme.shapeHover.x.a + this._theme.shapeHover.y.a + this._theme.shapeHover.z.a + this._theme.shapeHover.xyz.a + this._theme.shapeHover.f.a) / 5; } /** * @type {boolean} * @protected */ get _dragging() { return !this._hoverAxis && !!this._selectedAxis; } /** * @param {MeshInstance} [meshInstance] - The mesh instance. * @returns {GizmoAxis | ''} - The axis. * @private */ _getAxis(meshInstance) { if (!meshInstance) { return ""; } return ( /** @type {GizmoAxis | ''} */ 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; } const remove = new Set(this._hovering); let changed = false; const add = (axis) => { if (remove.has(axis)) { remove.delete(axis); } else { this._hovering.add(axis); this._shapes[axis]?.hover(true); changed = true; } }; this._hoverAxis = this._getAxis(meshInstance); this._hoverIsPlane = this._getIsPlane(meshInstance); if (this._hoverAxis) { if (this._hoverAxis === "xyz") { add("x"); add("y"); add("z"); add("xyz"); } else if (this._hoverIsPlane) { switch (this._hoverAxis) { case "x": add("y"); add("z"); add("yz"); break; case "y": add("x"); add("z"); add("xz"); break; case "z": add("x"); add("y"); add("xy"); break; } } else { add(this._hoverAxis); } } for (const axis of remove) { this._hovering.delete(axis); this._shapes[axis]?.hover(false); changed = true; } if (changed) { this._renderUpdate = true; } } /** * @param {Vec3} mouseWPos - The mouse world position. * @returns {Ray} - The ray. * @protected */ _createRay(mouseWPos) { if (this._camera.projection === PROJECTION_PERSPECTIVE) { ray.origin.copy(this._camera.entity.getPosition()); ray.direction.sub2(mouseWPos, ray.origin).normalize(); return ray; } const orthoDepth = this._camera.farClip - this._camera.nearClip; ray.origin.sub2(mouseWPos, v1.copy(this._camera.entity.forward).mulScalar(orthoDepth)); ray.direction.copy(this._camera.entity.forward); return ray; } /** * @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) { const facingDir = v1.copy(this.facingDir); const normal = plane.normal.set(0, 0, 0); if (isFacing) { normal.copy(this._camera.entity.forward).mulScalar(-1); } else { normal[axis] = 1; this._rootStartRot.transformVector(normal, normal); if (isLine) { v2.cross(normal, facingDir).normalize(); normal.cross(v2, normal).normalize(); } } return plane.setFromPointNormal(this._rootStartPos, normal); } /** * @param {string} axis - The axis * @param {Vec3} dir - The direction * @returns {Vec3} - The direction * @protected */ _dirFromAxis(axis, dir) { if (axis === "f") { 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(point2, axis) { v1.set(0, 0, 0); v1[axis] = 1; point2.copy(v1.mulScalar(v1.dot(point2))); const v = point2[axis]; point2.set(0, 0, 0); point2[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 (space is {@link Gizmo#coordSpace}). * @protected */ _screenToPoint(x, y, isFacing = false, isLine = false) { const mouseWPos = this._camera.screenToWorld(x, y, 1); const axis = this._selectedAxis; const ray2 = this._createRay(mouseWPos); const plane2 = this._createPlane(axis, isFacing, isLine); if (!plane2.intersectsRay(ray2, point)) { return point; } 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. * @protected */ _drawGuideLines(pos, rot, activeAxis, activeIsPlane) { for (const axis of AXES) { if (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); } } } } /** * @param {Vec3} pos - The position. * @param {Quat} rot - The rotation. * @param {'x' | 'y' | 'z'} axis - The axis. * @protected */ _drawSpanLine(pos, rot, axis) { const dir = this._dirFromAxis(axis, v1); const base = this._theme.guideBase[axis]; const from = v1.copy(dir).mulScalar(this._camera.farClip - this._camera.nearClip); const to = v2.copy(from).mulScalar(-1); rot.transformVector(from, from).add(pos); rot.transformVector(to, to).add(pos); if (this._theme.guideOcclusion < 1) { const occluded = color.copy(base); occluded.a *= 1 - this._theme.guideOcclusion; this._app.drawLine(from, to, occluded, false, this._layer); } if (base.a !== 0) { this._app.drawLine(from, to, base, true); } } /** @protected */ _createTransform() { for (const key in this._shapes) { const shape = this._shapes[key]; this.root.addChild(shape.entity); this.intersectShapes.push(shape); for (let i = 0; i < shape.meshInstances.length; i++) { this._shapeMap.set(shape.meshInstances[i], shape); } } } /** * Set the shape to be enabled or disabled. * * @param {GizmoAxis | 'face'} shapeAxis - The shape axis. * @param {boolean} enabled - The enabled state of shape. */ enableShape(shapeAxis, enabled) { if (shapeAxis === "face") { Debug.deprecated('"face" literal is deprecated use "f" literal instead'); shapeAxis = "f"; } const shape = this._shapes[shapeAxis]; if (!shape) { return; } shape.disabled = !enabled; } /** * Get the enabled state of the shape. * * @param {GizmoAxis | 'face'} shapeAxis - The shape axis. Can be: * @returns {boolean} - Then enabled state of the shape */ isShapeEnabled(shapeAxis) { if (shapeAxis === "face") { Debug.deprecated('"face" literal is deprecated use "f" literal instead'); shapeAxis = "f"; } const shape = this._shapes[shapeAxis]; if (!shape) { return false; } return !shape.disabled; } /** * Sets the theme or partial theme for the gizmo. * * @param {{ [K in keyof GizmoTheme]?: Partial<GizmoTheme[K]> }} partial - The partial theme to set. */ setTheme(partial) { const theme = { ...this._theme, ...partial }; if (typeof theme !== "object" || typeof theme.shapeBase !== "object" || typeof theme.shapeHover !== "object" || typeof theme.guideBase !== "object") { return; } for (const axis in theme.shapeBase) { if (theme.shapeBase[axis] instanceof Color) { this._theme.shapeBase[axis].copy(theme.shapeBase[axis]); } } for (const axis in theme.shapeHover) { if (theme.shapeHover[axis] instanceof Color) { this._theme.shapeHover[axis].copy(theme.shapeHover[axis]); } } for (const axis in theme.guideBase) { if (theme.guideBase[axis] instanceof Color) { this._theme.guideBase[axis].copy(theme.guideBase[axis]); } } if (typeof theme.guideOcclusion === "number") { this._theme.guideOcclusion = math.clamp(theme.guideOcclusion, 0, 1); } if (theme.disabled instanceof Color) { this._theme.disabled.copy(theme.disabled); } for (const name in this._shapes) { this._shapes[name].hover(!!this._hoverAxis); } } /** @override */ prerender() { super.prerender(); if (!this.enabled) { return; } const gizmoPos = this.root.getLocalPosition(); const gizmoRot = this.root.getRotation(); const activeAxis = this._hoverAxis || this._selectedAxis; const activeIsPlane = this._hoverIsPlane || this._selectedIsPlane; this._drawGuideLines(gizmoPos, gizmoRot, activeAxis, activeIsPlane); } /** @override */ destroy() { super.destroy(); for (const key in this._shapes) { this._shapes[key].destroy(); } } }; /** * Fired when the transformation has started. * * @event * @example * const gizmo = new pc.TransformGizmo(camera, layer); * gizmo.on('transform:start', () => { * console.log('Transformation started'); * }); */ __publicField(_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})`); * }); */ __publicField(_TransformGizmo, "EVENT_TRANSFORMMOVE", "transform:move"); /** * Fired when the transformation has ended. * * @event * @example * const gizmo = new pc.TransformGizmo(camera, layer); * gizmo.on('transform:end', () => { * console.log('Transformation ended'); * }); */ __publicField(_TransformGizmo, "EVENT_TRANSFORMEND", "transform:end"); let TransformGizmo = _TransformGizmo; export { TransformGizmo };