123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- import { Observable } from "../Misc/observable.js";
- import { Quaternion, Matrix, Vector3, TmpVectors } from "../Maths/math.vector.js";
- import { Color3 } from "../Maths/math.color.js";
- import "../Meshes/Builders/linesBuilder.js";
- import { Mesh } from "../Meshes/mesh.js";
- import { PointerDragBehavior } from "../Behaviors/Meshes/pointerDragBehavior.js";
- import { Gizmo } from "./gizmo.js";
- import { UtilityLayerRenderer } from "../Rendering/utilityLayerRenderer.js";
- import { StandardMaterial } from "../Materials/standardMaterial.js";
- import { ShaderMaterial } from "../Materials/shaderMaterial.js";
- import { Effect } from "../Materials/effect.js";
- import { CreatePlane } from "../Meshes/Builders/planeBuilder.js";
- import { CreateTorus } from "../Meshes/Builders/torusBuilder.js";
- import { Epsilon } from "../Maths/math.constants.js";
- import { Logger } from "../Misc/logger.js";
- /**
- * Single plane rotation gizmo
- */
- export class PlaneRotationGizmo extends Gizmo {
- /** Default material used to render when gizmo is not disabled or hovered */
- get coloredMaterial() {
- return this._coloredMaterial;
- }
- /** Material used to render when gizmo is hovered with mouse */
- get hoverMaterial() {
- return this._hoverMaterial;
- }
- /** Color used to render the drag angle sector when gizmo is rotated with mouse */
- set rotationColor(color) {
- this._rotationShaderMaterial.setColor3("rotationColor", color);
- }
- /** Material used to render when gizmo is disabled. typically grey. */
- get disableMaterial() {
- return this._disableMaterial;
- }
- /**
- * Creates a PlaneRotationGizmo
- * @param planeNormal The normal of the plane which the gizmo will be able to rotate on
- * @param color The color of the gizmo
- * @param gizmoLayer The utility layer the gizmo will be added to
- * @param tessellation Amount of tessellation to be used when creating rotation circles
- * @param parent
- * @param useEulerRotation Use and update Euler angle instead of quaternion
- * @param thickness display gizmo axis thickness
- * @param hoverColor The color of the gizmo when hovering over and dragging
- * @param disableColor The Color of the gizmo when its disabled
- */
- constructor(planeNormal, color = Color3.Gray(), gizmoLayer = UtilityLayerRenderer.DefaultUtilityLayer, tessellation = 32, parent = null,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- useEulerRotation = false, thickness = 1, hoverColor = Color3.Yellow(), disableColor = Color3.Gray()) {
- super(gizmoLayer);
- this._pointerObserver = null;
- /**
- * Rotation distance in radians that the gizmo will snap to (Default: 0)
- */
- this.snapDistance = 0;
- /**
- * Event that fires each time the gizmo snaps to a new location.
- * * snapDistance is the change in distance
- */
- this.onSnapObservable = new Observable();
- /**
- * Accumulated relative angle value for rotation on the axis. Reset to 0 when a dragStart occurs
- */
- this.angle = 0;
- /**
- * Custom sensitivity value for the drag strength
- */
- this.sensitivity = 1;
- this._isEnabled = true;
- this._parent = null;
- this._dragging = false;
- this._angles = new Vector3();
- this._parent = parent;
- // Create Material
- this._coloredMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
- this._coloredMaterial.diffuseColor = color;
- this._coloredMaterial.specularColor = color.subtract(new Color3(0.1, 0.1, 0.1));
- this._hoverMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
- this._hoverMaterial.diffuseColor = hoverColor;
- this._hoverMaterial.specularColor = hoverColor;
- this._disableMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
- this._disableMaterial.diffuseColor = disableColor;
- this._disableMaterial.alpha = 0.4;
- // Build mesh on root node
- this._gizmoMesh = new Mesh("", gizmoLayer.utilityLayerScene);
- const { rotationMesh, collider } = this._createGizmoMesh(this._gizmoMesh, thickness, tessellation);
- // Setup Rotation Circle
- this._rotationDisplayPlane = CreatePlane("rotationDisplay", {
- size: 0.6,
- updatable: false,
- }, this.gizmoLayer.utilityLayerScene);
- this._rotationDisplayPlane.rotation.z = Math.PI * 0.5;
- this._rotationDisplayPlane.parent = this._gizmoMesh;
- this._rotationDisplayPlane.setEnabled(false);
- Effect.ShadersStore["rotationGizmoVertexShader"] = PlaneRotationGizmo._RotationGizmoVertexShader;
- Effect.ShadersStore["rotationGizmoFragmentShader"] = PlaneRotationGizmo._RotationGizmoFragmentShader;
- this._rotationShaderMaterial = new ShaderMaterial("shader", this.gizmoLayer.utilityLayerScene, {
- vertex: "rotationGizmo",
- fragment: "rotationGizmo",
- }, {
- attributes: ["position", "uv"],
- uniforms: ["worldViewProjection", "angles", "rotationColor"],
- });
- this._rotationShaderMaterial.backFaceCulling = false;
- this.rotationColor = hoverColor;
- this._rotationDisplayPlane.material = this._rotationShaderMaterial;
- this._rotationDisplayPlane.visibility = 0.999;
- this._gizmoMesh.lookAt(this._rootMesh.position.add(planeNormal));
- this._rootMesh.addChild(this._gizmoMesh, Gizmo.PreserveScaling);
- this._gizmoMesh.scaling.scaleInPlace(1 / 3);
- // Add drag behavior to handle events when the gizmo is dragged
- this.dragBehavior = new PointerDragBehavior({ dragPlaneNormal: planeNormal });
- this.dragBehavior.moveAttached = false;
- this.dragBehavior.maxDragAngle = PlaneRotationGizmo.MaxDragAngle;
- this.dragBehavior._useAlternatePickedPointAboveMaxDragAngle = true;
- this._rootMesh.addBehavior(this.dragBehavior);
- // Closures for drag logic
- const lastDragPosition = new Vector3();
- const rotationMatrix = new Matrix();
- const planeNormalTowardsCamera = new Vector3();
- let localPlaneNormalTowardsCamera = new Vector3();
- this.dragBehavior.onDragStartObservable.add((e) => {
- if (this.attachedNode) {
- lastDragPosition.copyFrom(e.dragPlanePoint);
- this._rotationDisplayPlane.setEnabled(true);
- this._rotationDisplayPlane.getWorldMatrix().invertToRef(rotationMatrix);
- Vector3.TransformCoordinatesToRef(e.dragPlanePoint, rotationMatrix, lastDragPosition);
- this._angles.x = Math.atan2(lastDragPosition.y, lastDragPosition.x) + Math.PI;
- this._angles.y = 0;
- this._angles.z = this.updateGizmoRotationToMatchAttachedMesh ? 1 : 0;
- this._dragging = true;
- lastDragPosition.copyFrom(e.dragPlanePoint);
- this._rotationShaderMaterial.setVector3("angles", this._angles);
- this.angle = 0;
- }
- });
- this.dragBehavior.onDragEndObservable.add(() => {
- this._dragging = false;
- this._rotationDisplayPlane.setEnabled(false);
- });
- const tmpSnapEvent = { snapDistance: 0 };
- let currentSnapDragDistance = 0;
- const tmpMatrix = new Matrix();
- const amountToRotate = new Quaternion();
- this.dragBehavior.onDragObservable.add((event) => {
- if (this.attachedNode) {
- // Calc angle over full 360 degree (https://stackoverflow.com/questions/43493711/the-angle-between-two-3d-vectors-with-a-result-range-0-360)
- const nodeScale = new Vector3(1, 1, 1);
- const nodeQuaternion = new Quaternion(0, 0, 0, 1);
- const nodeTranslation = new Vector3(0, 0, 0);
- this.attachedNode.getWorldMatrix().decompose(nodeScale, nodeQuaternion, nodeTranslation);
- // uniform scaling of absolute value of components
- // (-1,1,1) is uniform but (1,1.001,1) is not
- const uniformScaling = Math.abs(Math.abs(nodeScale.x) - Math.abs(nodeScale.y)) <= Epsilon && Math.abs(Math.abs(nodeScale.x) - Math.abs(nodeScale.z)) <= Epsilon;
- if (!uniformScaling && this.updateGizmoRotationToMatchAttachedMesh) {
- Logger.Warn("Unable to use a rotation gizmo matching mesh rotation with non uniform scaling. Use uniform scaling or set updateGizmoRotationToMatchAttachedMesh to false.");
- return;
- }
- nodeQuaternion.normalize();
- const nodeTranslationForOperation = this.updateGizmoPositionToMatchAttachedMesh ? nodeTranslation : this._rootMesh.absolutePosition;
- const newVector = event.dragPlanePoint.subtract(nodeTranslationForOperation).normalize();
- const originalVector = lastDragPosition.subtract(nodeTranslationForOperation).normalize();
- const cross = Vector3.Cross(newVector, originalVector);
- const dot = Vector3.Dot(newVector, originalVector);
- let angle = Math.atan2(cross.length(), dot) * this.sensitivity;
- planeNormalTowardsCamera.copyFrom(planeNormal);
- localPlaneNormalTowardsCamera.copyFrom(planeNormal);
- if (this.updateGizmoRotationToMatchAttachedMesh) {
- nodeQuaternion.toRotationMatrix(rotationMatrix);
- localPlaneNormalTowardsCamera = Vector3.TransformCoordinates(planeNormalTowardsCamera, rotationMatrix);
- }
- // Flip up vector depending on which side the camera is on
- let cameraFlipped = false;
- if (gizmoLayer.utilityLayerScene.activeCamera) {
- const camVec = gizmoLayer.utilityLayerScene.activeCamera.position.subtract(nodeTranslationForOperation).normalize();
- if (Vector3.Dot(camVec, localPlaneNormalTowardsCamera) > 0) {
- planeNormalTowardsCamera.scaleInPlace(-1);
- localPlaneNormalTowardsCamera.scaleInPlace(-1);
- cameraFlipped = true;
- }
- }
- const halfCircleSide = Vector3.Dot(localPlaneNormalTowardsCamera, cross) > 0.0;
- if (halfCircleSide) {
- angle = -angle;
- }
- TmpVectors.Vector3[0].set(angle, 0, 0);
- if (!this.dragBehavior.validateDrag(TmpVectors.Vector3[0])) {
- angle = 0;
- }
- // Snapping logic
- let snapped = false;
- if (this.snapDistance != 0) {
- currentSnapDragDistance += angle;
- if (Math.abs(currentSnapDragDistance) > this.snapDistance) {
- let dragSteps = Math.floor(Math.abs(currentSnapDragDistance) / this.snapDistance);
- if (currentSnapDragDistance < 0) {
- dragSteps *= -1;
- }
- currentSnapDragDistance = currentSnapDragDistance % this.snapDistance;
- angle = this.snapDistance * dragSteps;
- snapped = true;
- }
- else {
- angle = 0;
- }
- }
- // Convert angle and axis to quaternion (http://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToQuaternion/index.htm)
- const quaternionCoefficient = Math.sin(angle / 2);
- amountToRotate.set(planeNormalTowardsCamera.x * quaternionCoefficient, planeNormalTowardsCamera.y * quaternionCoefficient, planeNormalTowardsCamera.z * quaternionCoefficient, Math.cos(angle / 2));
- // If the meshes local scale is inverted (eg. loaded gltf file parent with z scale of -1) the rotation needs to be inverted on the y axis
- if (tmpMatrix.determinant() > 0) {
- const tmpVector = new Vector3();
- amountToRotate.toEulerAnglesToRef(tmpVector);
- Quaternion.RotationYawPitchRollToRef(tmpVector.y, -tmpVector.x, -tmpVector.z, amountToRotate);
- }
- if (this.updateGizmoRotationToMatchAttachedMesh) {
- // Rotate selected mesh quaternion over fixed axis
- nodeQuaternion.multiplyToRef(amountToRotate, nodeQuaternion);
- nodeQuaternion.normalize();
- // recompose matrix
- Matrix.ComposeToRef(nodeScale, nodeQuaternion, nodeTranslation, this.attachedNode.getWorldMatrix());
- }
- else {
- // Rotate selected mesh quaternion over rotated axis
- amountToRotate.toRotationMatrix(TmpVectors.Matrix[0]);
- const translation = this.attachedNode.getWorldMatrix().getTranslation();
- this.attachedNode.getWorldMatrix().multiplyToRef(TmpVectors.Matrix[0], this.attachedNode.getWorldMatrix());
- this.attachedNode.getWorldMatrix().setTranslation(translation);
- }
- lastDragPosition.copyFrom(event.dragPlanePoint);
- if (snapped) {
- tmpSnapEvent.snapDistance = angle;
- this.onSnapObservable.notifyObservers(tmpSnapEvent);
- }
- this._angles.y += angle;
- this.angle += cameraFlipped ? -angle : angle;
- this._rotationShaderMaterial.setVector3("angles", this._angles);
- this._matrixChanged();
- }
- });
- const light = gizmoLayer._getSharedGizmoLight();
- light.includedOnlyMeshes = light.includedOnlyMeshes.concat(this._rootMesh.getChildMeshes(false));
- const cache = {
- colliderMeshes: [collider],
- gizmoMeshes: [rotationMesh],
- material: this._coloredMaterial,
- hoverMaterial: this._hoverMaterial,
- disableMaterial: this._disableMaterial,
- active: false,
- dragBehavior: this.dragBehavior,
- };
- this._parent?.addToAxisCache(this._gizmoMesh, cache);
- this._pointerObserver = gizmoLayer.utilityLayerScene.onPointerObservable.add((pointerInfo) => {
- if (this._customMeshSet) {
- return;
- }
- // updating here the maxangle because ondragstart is too late (value already used) and the updated value is not taken into account
- this.dragBehavior.maxDragAngle = PlaneRotationGizmo.MaxDragAngle;
- this._isHovered = !!(cache.colliderMeshes.indexOf(pointerInfo?.pickInfo?.pickedMesh) != -1);
- if (!this._parent) {
- const material = cache.dragBehavior.enabled ? (this._isHovered || this._dragging ? this._hoverMaterial : this._coloredMaterial) : this._disableMaterial;
- this._setGizmoMeshMaterial(cache.gizmoMeshes, material);
- }
- });
- this.dragBehavior.onEnabledObservable.add((newState) => {
- this._setGizmoMeshMaterial(cache.gizmoMeshes, newState ? this._coloredMaterial : this._disableMaterial);
- });
- }
- /**
- * @internal
- * Create Geometry for Gizmo
- * @param parentMesh
- * @param thickness
- * @param tessellation
- * @returns
- */
- _createGizmoMesh(parentMesh, thickness, tessellation) {
- const collider = CreateTorus("ignore", {
- diameter: 0.6,
- thickness: 0.03 * thickness,
- tessellation,
- }, this.gizmoLayer.utilityLayerScene);
- collider.visibility = 0;
- const rotationMesh = CreateTorus("", {
- diameter: 0.6,
- thickness: 0.005 * thickness,
- tessellation,
- }, this.gizmoLayer.utilityLayerScene);
- rotationMesh.material = this._coloredMaterial;
- // Position arrow pointing in its drag axis
- rotationMesh.rotation.x = Math.PI / 2;
- collider.rotation.x = Math.PI / 2;
- parentMesh.addChild(rotationMesh, Gizmo.PreserveScaling);
- parentMesh.addChild(collider, Gizmo.PreserveScaling);
- return { rotationMesh, collider };
- }
- _attachedNodeChanged(value) {
- if (this.dragBehavior) {
- this.dragBehavior.enabled = value ? true : false;
- }
- }
- /**
- * If the gizmo is enabled
- */
- set isEnabled(value) {
- this._isEnabled = value;
- if (!value) {
- this.attachedMesh = null;
- }
- else {
- if (this._parent) {
- this.attachedMesh = this._parent.attachedMesh;
- }
- }
- }
- get isEnabled() {
- return this._isEnabled;
- }
- /**
- * Disposes of the gizmo
- */
- dispose() {
- this.onSnapObservable.clear();
- this.gizmoLayer.utilityLayerScene.onPointerObservable.remove(this._pointerObserver);
- this.dragBehavior.detach();
- if (this._gizmoMesh) {
- this._gizmoMesh.dispose();
- }
- if (this._rotationDisplayPlane) {
- this._rotationDisplayPlane.dispose();
- }
- if (this._rotationShaderMaterial) {
- this._rotationShaderMaterial.dispose();
- }
- [this._coloredMaterial, this._hoverMaterial, this._disableMaterial].forEach((matl) => {
- if (matl) {
- matl.dispose();
- }
- });
- super.dispose();
- }
- }
- /**
- * The maximum angle between the camera and the rotation allowed for interaction
- * If a rotation plane appears 'flat', a lower value allows interaction.
- */
- PlaneRotationGizmo.MaxDragAngle = (Math.PI * 9) / 20;
- PlaneRotationGizmo._RotationGizmoVertexShader = `
- precision highp float;
- attribute vec3 position;
- attribute vec2 uv;
- uniform mat4 worldViewProjection;
- varying vec3 vPosition;
- varying vec2 vUV;
- void main(void) {
- gl_Position = worldViewProjection * vec4(position, 1.0);
- vUV = uv;
- }`;
- PlaneRotationGizmo._RotationGizmoFragmentShader = `
- precision highp float;
- varying vec2 vUV;
- varying vec3 vPosition;
- uniform vec3 angles;
- uniform vec3 rotationColor;
- #define twopi 6.283185307
- void main(void) {
- vec2 uv = vUV - vec2(0.5);
- float angle = atan(uv.y, uv.x) + 3.141592;
- float delta = gl_FrontFacing ? angles.y : -angles.y;
- float begin = angles.x - delta * angles.z;
- float start = (begin < (begin + delta)) ? begin : (begin + delta);
- float end = (begin > (begin + delta)) ? begin : (begin + delta);
- float len = sqrt(dot(uv,uv));
- float opacity = 1. - step(0.5, len);
- float base = abs(floor(start / twopi)) * twopi;
- start += base;
- end += base;
- float intensity = 0.;
- for (int i = 0; i < 5; i++)
- {
- intensity += max(step(start, angle) - step(end, angle), 0.);
- angle += twopi;
- }
- gl_FragColor = vec4(rotationColor, min(intensity * 0.25, 0.8)) * opacity;
- }
- `;
- //# sourceMappingURL=planeRotationGizmo.js.map
|