playcanvas
Version:
PlayCanvas WebGL game engine
422 lines (419 loc) • 12.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 { COLOR_GRAY, COLOR_BLUE, COLOR_GREEN, COLOR_RED, color4from3 } from './color.js';
import { Gizmo } from './gizmo.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 = [
'x',
'y',
'z'
];
class TransformGizmo extends Gizmo {
static{
this.EVENT_TRANSFORMSTART = 'transform:start';
}
static{
this.EVENT_TRANSFORMMOVE = 'transform:move';
}
static{
this.EVENT_TRANSFORMEND = 'transform:end';
}
constructor(camera, layer, name = 'gizmo:transform'){
super(camera, layer, name), 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()
}, this._rootStartPos = new Vec3(), this._rootStartRot = new Quat(), this._shapes = {}, this._shapeMap = new Map(), this._hovering = new Set(), this._hoverAxis = '', this._hoverIsPlane = false, this._selectedAxis = '', this._selectedIsPlane = false, this._selectionStartPoint = new Vec3(), this._snap = false, this.snapIncrement = 1, 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 point = this._screenToPoint(x, y);
this._selectionStartPoint.copy(point);
this.fire(TransformGizmo.EVENT_TRANSFORMSTART, point, 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 point = this._screenToPoint(x, y);
this.fire(TransformGizmo.EVENT_TRANSFORMMOVE, point, 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.snap = false;
this._hoverAxis = '';
this._hoverIsPlane = false;
this._hover();
this.fire(Gizmo.EVENT_POINTERUP);
});
}
set snap(value) {
this._snap = this.enabled && value;
}
get snap() {
return this._snap;
}
get theme() {
return this._theme;
}
set xAxisColor(value) {
this.setTheme({
shapeBase: {
x: value
},
shapeHover: {
x: color4from3(value, this.colorAlpha)
},
guideBase: {
x: value
},
guideOcclusion: 1
});
}
get xAxisColor() {
return this._theme.shapeBase.x;
}
set yAxisColor(value) {
this.setTheme({
shapeBase: {
y: value
},
shapeHover: {
y: color4from3(value, this.colorAlpha)
},
guideBase: {
y: value
},
guideOcclusion: 1
});
}
get yAxisColor() {
return this._theme.shapeBase.y;
}
set zAxisColor(value) {
this.setTheme({
shapeBase: {
z: value
},
shapeHover: {
z: color4from3(value, this.colorAlpha)
},
guideBase: {
z: value
},
guideOcclusion: 1
});
}
get zAxisColor() {
return this._theme.shapeBase.z;
}
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)
}
});
}
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;
}
get _dragging() {
return !this._hoverAxis && !!this._selectedAxis;
}
_getAxis(meshInstance) {
if (!meshInstance) {
return '';
}
return meshInstance.node.name.split(':')[1];
}
_getIsPlane(meshInstance) {
if (!meshInstance) {
return false;
}
return meshInstance.node.name.indexOf('plane') !== -1;
}
_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;
}
}
_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;
}
_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);
}
_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;
}
_projectToAxis(point, axis) {
v1.set(0, 0, 0);
v1[axis] = 1;
point.copy(v1.mulScalar(v1.dot(point)));
const v = point[axis];
point.set(0, 0, 0);
point[axis] = v;
}
_screenToPoint(x, y, isFacing = false, isLine = false) {
const mouseWPos = this._camera.screenToWorld(x, y, 1);
const axis = this._selectedAxis;
const ray = this._createRay(mouseWPos);
const plane = this._createPlane(axis, isFacing, isLine);
if (!plane.intersectsRay(ray, point)) {
return point;
}
return point;
}
_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);
}
}
}
}
_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);
}
}
_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);
}
}
}
enableShape(shapeAxis, enabled) {
if (shapeAxis === 'face') {
shapeAxis = 'f';
}
const shape = this._shapes[shapeAxis];
if (!shape) {
return;
}
shape.disabled = !enabled;
}
isShapeEnabled(shapeAxis) {
if (shapeAxis === 'face') {
shapeAxis = 'f';
}
const shape = this._shapes[shapeAxis];
if (!shape) {
return false;
}
return !shape.disabled;
}
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);
}
}
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);
}
destroy() {
super.destroy();
for(const key in this._shapes){
this._shapes[key].destroy();
}
}
}
export { TransformGizmo };