playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
673 lines (672 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 { 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
};