UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

444 lines (441 loc) 15 kB
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 };