control_MouseNavigation.ts

import * as math from "../math";
import {Control, IControlParams} from "./Control";
import {input} from "../input/input";
import {Key} from "../Lock";
import {LonLat} from "../LonLat";
import {Mat4} from "../math/Mat4";
import {Quat} from "../math/Quat";
import {Ray} from "../math/Ray";
import {Sphere} from "../bv/Sphere";
import {Vec3} from "../math/Vec3";
import {Vec2} from "../math/Vec2";
import {Planet} from "../scene/Planet";
import {PlanetCamera} from "../camera/PlanetCamera";
import {IMouseState} from "../renderer/RendererEvents";

export interface IStepForward {
    eye: Vec3;
    v: Vec3;
    u: Vec3;
    n: Vec3;
}

interface IMouseNavigationParams extends IControlParams {
    minSlope?: number;
}

/**
 * Mouse planet camera dragging control.
 */
export class MouseNavigation extends Control {
    protected grabbedPoint: Vec3;
    protected _eye0: Vec3;
    protected pointOnEarth: Vec3;
    protected earthUp: Vec3;
    public inertia: number;
    protected grabbedSpheroid: Sphere;
    protected qRot: Quat;
    protected scaleRot: number;
    protected distDiff: number;
    protected stepsCount: number;
    protected stepsForward: IStepForward[] | null;
    protected stepIndex: number;
    protected _lmbDoubleClickActive: boolean;
    public minSlope: number;
    protected _wheelDirection: number;
    protected _keyLock: Key;
    protected _deactivate = false;
    protected _shiftBusy = false;

    constructor(options: IMouseNavigationParams = {}) {
        super(options);

        this._name = "mouseNavigation";

        this.grabbedPoint = new Vec3();
        this._eye0 = new Vec3();
        this.pointOnEarth = new Vec3();
        this.earthUp = new Vec3();
        this.inertia = 0.007;
        this.grabbedSpheroid = new Sphere();
        this.qRot = new Quat();
        this.scaleRot = 0.0;

        this.distDiff = 0.3;
        this.stepsCount = 8;
        this.stepsForward = null;
        this.stepIndex = 0;

        this._lmbDoubleClickActive = true;

        this.minSlope = options.minSlope || 0.1;

        this._wheelDirection = +1;

        this._keyLock = new Key();
    }

    static getMovePointsFromPixelTerrain(cam: PlanetCamera, planet: Planet, stepsCount: number, delta: number, point: Vec2, forward: boolean, dir?: Vec3 | null): IStepForward[] | undefined {
        const steps: IStepForward[] = [];

        let eye = cam.eye.clone(),
            n = cam._b.clone(),
            u = cam._r.clone(),
            v = cam._u.clone();

        let a = planet.getCartesianFromPixelTerrain(point);

        if (!a) {
            a = planet.getCartesianFromPixelTerrain(planet.renderer!.handler.getCenter());
        }

        if (a) {
            if (!dir) {
                dir = Vec3.sub(a, cam.eye).normalize();
            }

            let d = (delta * cam.eye.distance(a)) / stepsCount;

            if (forward) {
                d = -1.25 * d;
            } else {
                d *= 2;
            }

            const scaled_n = n.scaleTo(d);

            const slope = dir.dot(cam.eye.normal().negate());

            if (slope >= 0.1) {
                const grabbedSpheroid = new Sphere();
                grabbedSpheroid.radius = a.length();

                let rotArr = [],
                    eyeArr = [];

                let breaked = false;
                for (let i = 0; i < stepsCount; i++) {
                    eye.addA(scaled_n);
                    const b = new Ray(eye, dir).hitSphere(grabbedSpheroid);
                    eyeArr[i] = eye.clone();
                    if (b) {
                        rotArr[i] = new Mat4().rotateBetweenVectors(a.normal(), b.normal());
                    } else {
                        breaked = true;
                        break;
                    }
                }

                if (!breaked) {
                    for (let i = 0; i < stepsCount; i++) {
                        let rot = rotArr[i];
                        steps[i] = {
                            eye: rot.mulVec3(eyeArr[i]),
                            v: rot.mulVec3(v),
                            u: rot.mulVec3(u),
                            n: rot.mulVec3(n)
                        };
                    }
                } else {
                    eye = cam.eye.clone();
                    for (let i = 0; i < stepsCount; i++) {
                        steps[i] = {
                            eye: eye.addA(scaled_n).clone(),
                            v: v,
                            u: u,
                            n: n,
                        };
                    }
                }
            } else {
                for (let i = 0; i < stepsCount; i++) {
                    steps[i] = {
                        eye: eye.addA(dir.scaleTo(-d)).clone(),
                        v: v,
                        u: u,
                        n: n,
                    };
                }
            }

            return steps;
        }
    }

    public override onactivate() {
        if (this.renderer) {
            this.renderer.events.on("mousewheel", this.onMouseWheel, this);
            this.renderer.events.on("lhold", this.onMouseLeftButtonDown, this);
            this.renderer.events.on("rhold", this.onMouseRightButtonDown, this);
            this.renderer.events.on("ldown", this.onMouseLeftButtonClick, this);
            this.renderer.events.on("lup", this.onMouseLeftButtonUp, this);
            this.renderer.events.on("rdown", this.onMouseRightButtonClick, this);
            this.renderer.events.on("draw", this.onDraw, this, -1000);
            this.renderer.events.on("mousemove", this.onMouseMove, this);
            this.renderer.events.on("mouseleave", this.onMouseLeave, this);
            this.renderer.events.on("mouseenter", this.onMouseEnter, this);

            if (this._lmbDoubleClickActive) {
                this.renderer.events.on("ldblclick", this.onMouseLeftButtonDoubleClick, this);
            }
        }
    }

    public override ondeactivate() {
        if (this.renderer) {
            this.renderer.events.off("mousewheel", this.onMouseWheel);
            this.renderer.events.off("lhold", this.onMouseLeftButtonDown);
            this.renderer.events.off("rhold", this.onMouseRightButtonDown);
            this.renderer.events.off("ldown", this.onMouseLeftButtonClick);
            this.renderer.events.off("lup", this.onMouseLeftButtonUp);
            this.renderer.events.off("rdown", this.onMouseRightButtonClick);
            this.renderer.events.off("draw", this.onDraw);
            this.renderer.events.off("ldblclick", this.onMouseLeftButtonDoubleClick);
            this.renderer.events.off("mouseleave", this.onMouseLeave);
            this.renderer.events.off("mouseenter", this.onMouseEnter);
        }
    }

    public activateDoubleClickZoom() {
        if (!this._lmbDoubleClickActive) {
            this._lmbDoubleClickActive = true;
            this.renderer && this.renderer.events.on("ldblclick", this.onMouseLeftButtonDoubleClick, this);
        }
    }

    public deactivateDoubleClickZoom() {
        if (this._lmbDoubleClickActive) {
            this._lmbDoubleClickActive = false;
            this.renderer && this.renderer.events.off("ldblclick", this.onMouseLeftButtonDoubleClick);
        }
    }

    protected onMouseEnter(e: IMouseState) {
        const renderEvents = this.renderer!.events;
        if (renderEvents.isKeyPressed(input.KEY_ALT)) {
            renderEvents.releaseKeys();
        }

        renderEvents.updateButtonsStates(e.sys!.buttons);
        if (renderEvents.mouseState.leftButtonDown) {
            this.renderer!.handler.canvas!.classList.add("ogGrabbingPoiner");
        } else {
            this.renderer!.handler.canvas!.classList.remove("ogGrabbingPoiner");
        }
    }

    protected onMouseLeave() {
        if (this.renderer!.events.mouseState.leftButtonDown) {
            this.scaleRot = 0;
        }
        this.renderer!.handler.canvas!.classList.remove("ogGrabbingPoiner");
    }

    protected onMouseWheel(e: IMouseState) {
        if (this.stepIndex) {
            return;
        }

        this.planet!.stopFlying();

        this.stopRotation();

        this._deactivate = true;

        this.lockPlanet(true);

        this.stepsForward = MouseNavigation.getMovePointsFromPixelTerrain(
            this.planet!.camera,
            this.planet!,
            this.stepsCount,
            this.distDiff,
            e.pos,
            e.wheelDelta > 0,
            e.direction
        ) || null;

        this._wheelDirection = e.wheelDelta;

        if (this.stepsForward) {
            this.stepIndex = this.stepsCount;
        }
    }

    public override oninit() {
        this.activate();
        if (this.renderer) {
            this.renderer.events.on("keyfree", input.KEY_ALT, this.onShiftFree, this);
            this.renderer.events.on("keyfree", input.KEY_PRINTSCREEN, this.onShiftFree, this);
        }
    }

    protected onMouseLeftButtonDoubleClick(e: IMouseState) {
        this.planet!.stopFlying();
        this.stopRotation();
        const p = this.planet!.getCartesianFromPixelTerrain(e.pos);
        if (p) {
            const cam = this.planet!.camera;
            let maxAlt = cam.maxAltitude + this.planet!.ellipsoid.polarSize;
            let minAlt = cam.minAltitude + this.planet!.ellipsoid.polarSize;
            const camAlt = cam.eye.length();
            const g = this.planet!.ellipsoid.cartesianToLonLat(p);
            if (camAlt > maxAlt || camAlt < minAlt) {
                this.planet!.flyLonLat(new LonLat(g.lon, g.lat))
                return;
            }

            if (this.renderer!.events.isKeyPressed(input.KEY_ALT)) {
                this.planet!.flyLonLat(
                    new LonLat(g.lon, g.lat, cam.eye.distance(p) * 2.0)
                );
            } else {
                this.planet!.flyLonLat(
                    new LonLat(g.lon, g.lat, cam.eye.distance(p) * 0.57)
                );
            }
        }
    }

    protected onMouseLeftButtonClick() {
        if (this._active) {
            this.renderer!.handler.canvas!.classList.add("ogGrabbingPoiner");
            this.grabbedPoint = this.planet!.getCartesianFromMouseTerrain()!;
            if (this.grabbedPoint) {
                this._eye0.copy(this.planet!.camera.eye);
                this.grabbedSpheroid.radius = this.grabbedPoint.length();
                this.stopRotation();
            }
        }
    }

    public stopRotation() {
        this.qRot.clear();
        this.freePlanet();
    }

    protected onMouseLeftButtonUp(e: IMouseState) {
        this.renderer!.handler.canvas!.classList.remove("ogGrabbingPoiner");
        if (e.x === e.prev_x && e.y === e.prev_y) {
            this.scaleRot = 0.0;
        }
    }

    protected onMouseLeftButtonDown(e: IMouseState) {
        if (this._active) {
            if (!this.grabbedPoint) {
                return;
            }

            this.planet!.stopFlying();

            if (e.moving) {
                let cam = this.planet!.camera;

                if (cam.slope > 0.2) {
                    const targetPoint = new Ray(cam.eye, e.direction).hitSphere(this.grabbedSpheroid);
                    if (targetPoint) {
                        this.scaleRot = 1.0;
                        this.qRot = Quat.getRotationBetweenVectors(
                            targetPoint.normal(),
                            this.grabbedPoint.normal()
                        );
                        let rot = this.qRot;
                        cam.eye = rot.mulVec3(cam.eye);
                        cam._u = rot.mulVec3(cam._u);
                        cam._r = rot.mulVec3(cam._r);
                        cam._b = rot.mulVec3(cam._b);
                    }
                } else {
                    let p0 = this.grabbedPoint,
                        p1 = Vec3.add(p0, cam._r),
                        p2 = Vec3.add(p0, p0.normal());

                    let px = new Vec3();
                    if (new Ray(cam.eye, e.direction).hitPlane(p0, p1, p2, px) === Ray.INSIDE) {
                        cam.eye = this._eye0.addA(px.subA(p0).negate());
                    }
                }
            }
        }
    }

    protected onMouseRightButtonClick(e: IMouseState) {
        this.stopRotation();
        this.planet!.stopFlying();
        this.pointOnEarth = this.planet!.getCartesianFromPixelTerrain(e.pos)!;
        if (this.pointOnEarth) {
            this.earthUp = this.pointOnEarth.normal();
        }
    }

    protected onMouseRightButtonDown(e: IMouseState) {
        const cam = this.planet!.camera;

        if (this.pointOnEarth && e.moving) {
            this.renderer!.controlsBag.scaleRot = 1.0;
            let l = (0.5 / cam.eye.distance(this.pointOnEarth)) * cam._lonLat.height * math.RADIANS;
            if (l > 0.007) {
                l = 0.007;
            } else if (l < 0.003) {
                l = 0.003;
            }
            cam.rotateHorizontal(l * (e.x - e.prev_x), false, this.pointOnEarth, this.earthUp);
            cam.rotateVertical(l * (e.y - e.prev_y), this.pointOnEarth, this.minSlope);
        }
    }

    public onShiftFree() {
        this._shiftBusy = false;
    }

    protected onMouseMove(e: IMouseState) {
        if (this._active && this.renderer!.events.isKeyPressed(input.KEY_ALT)) {
            if (!this._shiftBusy) {
                this._shiftBusy = true;
                this.onMouseRightButtonClick(e);
            }

            this.onMouseRightButtonDown(e);
        }
    }

    protected onDraw() {
        if (this._active) {
            const r = this.renderer!;
            const cam = this.planet!.camera;
            let prevEye = cam.eye.clone();

            if (this.stepIndex) {
                r.controlsBag.scaleRot = 1.0;
                const sf = this.stepsForward![this.stepsCount - this.stepIndex--];
                cam.eye = sf.eye;
                cam._u = sf.v;
                cam._r = sf.u;
                cam._b = sf.n;
            } else {
                if (this._deactivate) {
                    this._deactivate = false;
                    this.freePlanet();
                }
            }

            if (r.events.mouseState.leftButtonDown || !this.scaleRot) {
                return;
            }

            this.scaleRot -= this.inertia;
            if (this.scaleRot <= 0.0) {
                this.scaleRot = 0.0;
            } else {
                r.controlsBag.scaleRot = this.scaleRot;
                let rot = this.qRot
                    .slerp(Quat.IDENTITY, 1.0 - this.scaleRot * this.scaleRot * this.scaleRot)
                    .normalize();
                if (!(rot.x || rot.y || rot.z)) {
                    this.scaleRot = 0.0;
                }
                cam.eye = rot.mulVec3(cam.eye);
                cam._u = rot.mulVec3(cam._u);
                cam._r = rot.mulVec3(cam._r);
                cam._b = rot.mulVec3(cam._b);
            }

            if (cam.eye.distance(prevEye) / cam.getAltitude() > 0.01) {
                this.lockPlanet();
            } else {
                this.freePlanet();
            }
        }
    }

    public lockPlanet(skipTerrain?: boolean) {
        this.planet!.layerLock.lock(this._keyLock);
        !skipTerrain && this.planet!.terrainLock.lock(this._keyLock);
        this.planet!._normalMapCreator.lock(this._keyLock);
    }

    public freePlanet() {
        this.planet!.layerLock.free(this._keyLock);
        this.planet!.terrainLock.free(this._keyLock);
        this.planet!._normalMapCreator.free(this._keyLock);
    }
}