playcanvas
Version:
PlayCanvas WebGL game engine
444 lines (441 loc) • 15 kB
JavaScript
import { math } from '../../core/math/math.js';
import { Color } from '../../core/math/color.js';
import { Quat } from '../../core/math/quat.js';
import { Vec2 } from '../../core/math/vec2.js';
import { Vec3 } from '../../core/math/vec3.js';
import { PROJECTION_PERSPECTIVE } from '../../scene/constants.js';
import { ArcShape } from './shape/arc-shape.js';
import { TransformGizmo } from './transform-gizmo.js';
import { MeshLine } from './mesh-line.js';
import { SphereShape } from './shape/sphere-shape.js';
const point = new Vec3();
const v1 = new Vec3();
const v2 = new Vec3();
const v3 = new Vec3();
const q1 = new Quat();
const q2 = new Quat();
const color = new Color();
const RING_FACING_EPSILON = 1e-4;
const UPDATE_EPSILON = 1e-6;
const AXES = [
'x',
'y',
'z'
];
class RotateGizmo extends TransformGizmo {
constructor(camera, layer){
super(camera, layer, 'gizmo:rotate'), this._shapes = {
z: new ArcShape(this._device, {
axis: 'z',
layers: [
this._layer.id
],
rotation: new Vec3(90, 0, 90),
defaultColor: this._theme.shapeBase.z,
hoverColor: this._theme.shapeHover.z,
disabledColor: this._theme.disabled,
sectorAngle: 180
}),
x: new ArcShape(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,
sectorAngle: 180
}),
y: new ArcShape(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,
sectorAngle: 180
}),
f: new ArcShape(this._device, {
axis: 'f',
layers: [
this._layer.id
],
defaultColor: this._theme.shapeBase.f,
hoverColor: this._theme.shapeHover.f,
disabledColor: this._theme.disabled,
ringRadius: 0.55
}),
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,
radius: 0.5
})
}, this._selectionStartAngle = 0, this._nodeLocalRotations = new Map(), this._nodeRotations = new Map(), this._nodeOffsets = new Map(), this._screenPos = new Vec2(), this._screenStartPos = new Vec2(), this._guideAngleStart = new Vec3(), this._guideAngleEnd = new Vec3(), this._facingDir = new Vec3(), this.snapIncrement = 5, this.rotationMode = 'absolute';
this.setTheme({
shapeBase: {
xyz: new Color(0, 0, 0, 0)
},
shapeHover: {
xyz: new Color(1, 1, 1, 0.2)
}
});
this._createTransform();
this._guideAngleLines = [
new MeshLine(this._app, this._layer),
new MeshLine(this._app, this._layer)
];
this._guideAngleLines.forEach((line)=>{
this._app.root.addChild(line.entity);
line.entity.enabled = false;
});
this.on(TransformGizmo.EVENT_TRANSFORMSTART, (point, x, y)=>{
this._screenPos.set(x, y);
this._screenStartPos.set(x, y);
this._selectionStartAngle = this._calculateArcAngle(point, x, y);
this._storeNodeRotations();
this._storeGuidePoints();
this._drag(true);
this._angleGuide(true);
});
this.on(TransformGizmo.EVENT_TRANSFORMMOVE, (point, x, y)=>{
const axis = this._selectedAxis;
if (!axis) {
return;
}
this._screenPos.set(x, y);
if (axis === 'xyz') {
const facingDir = v1.copy(this.facingDir);
const delta = v2.copy(point).sub(this._selectionStartPoint);
const angleAxis = v1.cross(facingDir, delta).normalize();
const angleDelta = this._screenPos.distance(this._screenStartPos);
this._setNodeRotations(axis, angleAxis, angleDelta);
} else {
let angleDelta = this._calculateArcAngle(point, x, y) - this._selectionStartAngle;
if (this.snap) {
angleDelta = Math.round(angleDelta / this.snapIncrement) * this.snapIncrement;
}
const angleAxis = this._dirFromAxis(axis, v1);
this._setNodeRotations(axis, angleAxis, angleDelta);
this._updateGuidePoints(angleDelta);
this._angleGuide(true);
}
});
this.on(TransformGizmo.EVENT_TRANSFORMEND, ()=>{
this._drag(false);
this._angleGuide(false);
});
this.on(TransformGizmo.EVENT_NODESDETACH, ()=>{
this._nodeLocalRotations.clear();
this._nodeRotations.clear();
this._nodeOffsets.clear();
});
}
set xyzTubeRadius(value) {
this._setDiskProp('tubeRadius', value);
}
get xyzTubeRadius() {
return this._shapes.x.tubeRadius;
}
set xyzRingRadius(value) {
this._setDiskProp('ringRadius', value);
}
get xyzRingRadius() {
return this._shapes.x.ringRadius;
}
set faceTubeRadius(value) {
this._shapes.f.tubeRadius = value;
}
get faceTubeRadius() {
return this._shapes.f.tubeRadius;
}
set faceRingRadius(value) {
this._shapes.f.ringRadius = value;
}
get faceRingRadius() {
return this._shapes.f.ringRadius;
}
set centerRadius(value) {
this._shapes.xyz.radius = value;
}
get centerRadius() {
return this._shapes.xyz.radius;
}
set ringTolerance(value) {
this._setDiskProp('tolerance', value);
this._shapes.f.tolerance = value;
}
get ringTolerance() {
return this._shapes.x.tolerance;
}
set angleGuideThickness(value) {
this._guideAngleLines[0].thickness = value;
this._guideAngleLines[1].thickness = value;
}
get angleGuideThickness() {
return this._guideAngleLines[0].thickness;
}
set orbitRotation(value) {
this.rotationMode = value ? 'orbit' : 'absolute';
}
get orbitRotation() {
return this.rotationMode === 'orbit';
}
_setDiskProp(prop, value) {
this._shapes.x[prop] = value;
this._shapes.y[prop] = value;
this._shapes.z[prop] = value;
}
_storeGuidePoints() {
const gizmoPos = this.root.getLocalPosition();
const axis = this._selectedAxis;
const isFacing = axis === 'f';
const scale = isFacing ? this.faceRingRadius : this.xyzRingRadius;
this._guideAngleStart.copy(this._selectionStartPoint).sub(gizmoPos).normalize();
this._guideAngleStart.mulScalar(scale);
this._guideAngleEnd.copy(this._guideAngleStart);
}
_updateGuidePoints(angleDelta) {
const axis = this._selectedAxis;
const isFacing = axis === 'f';
if (isFacing) {
v1.copy(this.facingDir);
} else {
v1.set(0, 0, 0);
v1[axis] = 1;
this._rootStartRot.transformVector(v1, v1);
}
q1.setFromAxisAngle(v1, angleDelta);
q1.transformVector(this._guideAngleStart, this._guideAngleEnd);
this._renderUpdate = true;
}
_angleGuide(state) {
const axis = this._selectedAxis;
if (state && this.dragMode !== 'show' && axis !== 'xyz') {
const gizmoPos = this.root.getLocalPosition();
const baseColor = this._theme.shapeHover[axis];
const startColor = color.copy(baseColor);
startColor.a *= 0.3;
this._guideAngleLines[0].draw(gizmoPos, v1.copy(this._guideAngleStart).add(gizmoPos), this._scale, startColor);
this._guideAngleLines[1].draw(gizmoPos, v1.copy(this._guideAngleEnd).add(gizmoPos), this._scale, baseColor);
this._guideAngleLines[0].entity.enabled = true;
this._guideAngleLines[1].entity.enabled = true;
} else {
this._guideAngleLines[0].entity.enabled = false;
this._guideAngleLines[1].entity.enabled = false;
}
}
_shapesLookAtCamera() {
if (this._camera.projection === PROJECTION_PERSPECTIVE) {
const dir = this._camera.entity.getPosition().sub(this.root.getPosition()).normalize();
const elev = Math.atan2(-dir.y, Math.sqrt(dir.x * dir.x + dir.z * dir.z)) * math.RAD_TO_DEG;
const azim = Math.atan2(-dir.x, -dir.z) * math.RAD_TO_DEG;
this._shapes.f.entity.setEulerAngles(-elev + 90, azim, 0);
} else {
q1.copy(this._camera.entity.getRotation()).getEulerAngles(v1);
this._shapes.f.entity.setEulerAngles(v1);
this._shapes.f.entity.rotateLocal(-90, 0, 0);
}
let angle, dot, sector;
const facingDir = v1.copy(this.facingDir);
q1.copy(this.root.getRotation()).invert().transformVector(facingDir, facingDir);
angle = Math.atan2(facingDir.z, facingDir.y) * math.RAD_TO_DEG;
this._shapes.x.entity.setLocalEulerAngles(0, angle - 90, -90);
angle = Math.atan2(facingDir.x, facingDir.z) * math.RAD_TO_DEG;
this._shapes.y.entity.setLocalEulerAngles(0, angle, 0);
angle = Math.atan2(facingDir.y, facingDir.x) * math.RAD_TO_DEG;
this._shapes.z.entity.setLocalEulerAngles(90, 0, angle + 90);
if (!this._dragging) {
dot = facingDir.dot(this.root.right);
sector = 1 - Math.abs(dot) > RING_FACING_EPSILON;
this._shapes.x.show(sector ? 'sector' : 'ring');
dot = facingDir.dot(this.root.up);
sector = 1 - Math.abs(dot) > RING_FACING_EPSILON;
this._shapes.y.show(sector ? 'sector' : 'ring');
dot = facingDir.dot(this.root.forward);
sector = 1 - Math.abs(dot) > RING_FACING_EPSILON;
this._shapes.z.show(sector ? 'sector' : 'ring');
}
if (!facingDir.equalsApprox(this._facingDir, UPDATE_EPSILON)) {
this._facingDir.copy(facingDir);
this._renderUpdate = true;
}
}
_drag(state) {
for(const axis in this._shapes){
const shape = this._shapes[axis];
if (!(shape instanceof ArcShape)) {
continue;
}
switch(this.dragMode){
case 'show':
{
break;
}
case 'hide':
{
shape.show(state ? axis === this._selectedAxis ? 'ring' : 'none' : 'sector');
continue;
}
case 'selected':
{
shape.show(state ? axis === this._selectedAxis ? 'ring' : 'sector' : 'sector');
break;
}
}
}
this._renderUpdate = true;
}
_storeNodeRotations() {
const gizmoPos = this.root.getLocalPosition();
for(let i = 0; i < this.nodes.length; i++){
const node = this.nodes[i];
this._nodeLocalRotations.set(node, node.getLocalRotation().clone());
this._nodeRotations.set(node, node.getRotation().clone());
this._nodeOffsets.set(node, node.getPosition().clone().sub(gizmoPos));
}
}
_setNodeRotations(axis, angleAxis, angleDelta) {
const gizmoPos = this.root.getLocalPosition();
q1.setFromAxisAngle(angleAxis, angleDelta);
for(let i = 0; i < this.nodes.length; i++){
const node = this.nodes[i];
if ((axis === 'x' || axis === 'y' || axis === 'z') && this._coordSpace === 'local') {
const rot = this._nodeLocalRotations.get(node);
if (!rot) {
continue;
}
q2.copy(rot).mul(q1);
node.setLocalRotation(q2);
} else {
const rot = this._nodeRotations.get(node);
if (!rot) {
continue;
}
const offset = this._nodeOffsets.get(node);
if (!offset) {
continue;
}
v1.copy(offset);
q1.transformVector(v1, v1);
q2.copy(q1).mul(rot);
node.setRotation(q2);
node.setPosition(v1.add(gizmoPos));
}
}
if (this._coordSpace === 'local') {
this._updateRotation();
}
}
_screenToPoint(x, y) {
const mouseWPos = this._camera.screenToWorld(x, y, 1);
const axis = this._selectedAxis;
const ray = this._createRay(mouseWPos);
const plane = this._createPlane(axis, axis === 'f' || axis === 'xyz', false);
if (!plane.intersectsRay(ray, point)) {
ray.direction.mulScalar(-1);
const intersection = plane.intersectsRay(ray, point);
ray.direction.mulScalar(-1);
if (!intersection) {
return point.copy(this.root.getLocalPosition());
}
}
return point;
}
_calculateArcAngle(point, x, y) {
const gizmoPos = this.root.getLocalPosition();
const axis = this._selectedAxis;
const plane = this._createPlane(axis, axis === 'f', false);
let angle = 0;
const facingDir = this.facingDir;
const facingDot = plane.normal.dot(facingDir);
switch(this.rotationMode){
case 'absolute':
{
this._camera.worldToScreen(gizmoPos, v2);
if (axis === 'f' || facingDot > 1 - RING_FACING_EPSILON) {
v1.set(this._screenStartPos.y >= v2.y ? 1 : -1, this._screenStartPos.x >= v2.x ? -1 : 1, 0).normalize();
} else {
const projDir = v1.cross(plane.normal, facingDir).normalize();
this._camera.worldToScreen(projDir.add(gizmoPos), v3);
v1.sub2(v3, v2).normalize();
}
v2.set(x, y, 0);
angle = v1.dot(v2);
break;
}
case 'orbit':
{
v1.sub2(point, gizmoPos);
switch(axis){
case 'x':
{
q1.copy(this._rootStartRot).invert().transformVector(v1, v1);
angle = Math.atan2(v1.z, v1.y) * math.RAD_TO_DEG;
break;
}
case 'y':
{
q1.copy(this._rootStartRot).invert().transformVector(v1, v1);
angle = Math.atan2(v1.x, v1.z) * math.RAD_TO_DEG;
break;
}
case 'z':
{
q1.copy(this._rootStartRot).invert().transformVector(v1, v1);
angle = Math.atan2(v1.y, v1.x) * math.RAD_TO_DEG;
break;
}
case 'f':
{
q1.copy(this._camera.entity.getRotation()).invert().transformVector(v1, v1);
angle = Math.sign(facingDot) * Math.atan2(v1.y, v1.x) * math.RAD_TO_DEG;
break;
}
}
const dir = v1.sub2(point, this._camera.entity.getPosition()).normalize();
const dot = dir.dot(this._camera.entity.forward);
if (dot < 0) {
angle += 180;
}
break;
}
}
return angle;
}
_drawGuideLines(pos, rot, activeAxis, activeIsPlane) {
for (const axis of AXES){
if (activeAxis === 'xyz') {
continue;
}
if (activeIsPlane) {
if (axis !== activeAxis) {
this._drawSpanLine(pos, rot, axis);
}
} else {
if (axis === activeAxis) {
this._drawSpanLine(pos, rot, axis);
}
}
}
}
prerender() {
super.prerender();
if (!this.enabled) {
return;
}
this._shapesLookAtCamera();
}
destroy() {
this._guideAngleLines.forEach((line)=>line.destroy());
super.destroy();
}
}
export { RotateGizmo };