UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

422 lines (419 loc) 12.2 kB
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 };