Reference Source

src/plugins/TransformControl/TransformControl.js

import {math} from "../../viewer/scene/math/math.js";

import {buildCylinderGeometry} from "../../viewer/scene/geometry/builders/buildCylinderGeometry.js";
import {buildTorusGeometry} from "../../viewer/scene/geometry/builders/buildTorusGeometry.js";

import {ReadableGeometry} from "../../viewer/scene/geometry/ReadableGeometry.js";
import {PhongMaterial} from "../../viewer/scene/materials/PhongMaterial.js";
import {EmphasisMaterial} from "../../viewer/scene/materials/EmphasisMaterial.js";
import {Node} from "../../viewer/scene/nodes/Node.js";
import {Mesh} from "../../viewer/scene/mesh/Mesh.js";
import {buildSphereGeometry} from "../../viewer/scene/geometry/builders/buildSphereGeometry.js";
import {worldToRTCPos} from "../../viewer/scene/math/rtcCoords.js";
import {transformToNode} from "../lib/ui/index.js";

const zeroVec = new Float64Array([0, 0, 1]);
const quat = new Float64Array(4);

/**
 * Controls a transformation with mouse and touch input.
 *
 * @private
 */
class TransformControl {

    /** @private */
    constructor(viewer) {
        const cameraControl = viewer.cameraControl;
        const scene = viewer.scene;
        const canvas = scene.canvas.canvas;

        // Builds the Entities that represent this Control.
        const NO_STATE_INHERIT = false;
        const arrowLength = 1.0;
        const handleRadius = 0.09;
        const tubeRadius = 0.01;

        const rootNode = new Node(scene, { // Root of Node graph that represents this control in the 3D scene
            position: [0, 0, 0],
            scale: [5, 5, 5],
            isObject: false
        });

        const pos = math.vec3();
        this._setPosition = (function() {
            const origin = math.vec3();
            const rtcPos = math.vec3();
            return function(p) {
                pos.set(p);
                worldToRTCPos(p, origin, rtcPos);
                rootNode.origin = origin;
                rootNode.position = rtcPos;
            };
        })();

        this._setQuaternion = q => { rootNode.quaternion = q; };

        const arrowGeometry = (radiusBottom, height) => new ReadableGeometry(rootNode, buildCylinderGeometry({
            radiusTop: 0.001,
            radiusBottom: radiusBottom,
            radialSegments: 32,
            heightSegments: 1,
            height: height,
            openEnded: false
        }));

        const tubeGeometry = (radius, height, radialSegments) => new ReadableGeometry(rootNode, buildCylinderGeometry({
            radiusTop: radius,
            radiusBottom: radius,
            radialSegments: radialSegments,
            heightSegments: 1,
            height: height,
            openEnded: false
        }));

        const torusGeometry = (tube, arcFraction, tubeSegments) => new ReadableGeometry(rootNode, buildTorusGeometry({
            radius: arrowLength - 0.2,
            tube: tube,
            radialSegments: 64,
            tubeSegments: tubeSegments,
            arc: (Math.PI * 2.0) * arcFraction
        }));

        const shapes = {// Reusable geometries

            curve:       torusGeometry(tubeRadius,   0.25, 14),
            curveHandle: torusGeometry(handleRadius, 0.25, 14),
            hoop:        torusGeometry(tubeRadius,   1,     8),

            arrowHead:       arrowGeometry(0.07, 0.2),
            arrowHeadBig:    arrowGeometry(0.09, 0.25),
            arrowHeadHandle: tubeGeometry(handleRadius, 0.37, 8),

            axis:       tubeGeometry(tubeRadius,   arrowLength, 20),
            axisHandle: tubeGeometry(handleRadius, arrowLength, 20)
        };

        const colorMaterial = (rgb) => new PhongMaterial(rootNode, {
            diffuse: rgb,
            emissive: rgb,
            ambient: [0.0, 0.0, 0.0],
            specular: [.6, .6, .3],
            shininess: 80,
            lineWidth: 2
        });

        const highlightMaterial = (rgb, fillAlpha) => new EmphasisMaterial(rootNode, {
            edges: false,
            fill: true,
            fillColor: rgb,
            fillAlpha: fillAlpha
        });

        const pickableMaterial = new PhongMaterial(rootNode, { // Invisible material for pickable handles, which define a pickable 3D area
            diffuse: [1, 1, 0],
            alpha: 0, // Invisible
            alphaMode: "blend"
        });

        const handlers = { };
        const addAxis = (rgb, hoopRot) => {
            const axisDirection = math.mulVec3Scalar(rgb, -1, math.vec3());
            const material = colorMaterial(rgb);

            const hoopMatrix = math.quaternionToRotationMat4(hoopRot, math.identityMat4());
            math.mulMat4(math.rotationMat4v(Math.PI, [0,1,0], math.mat4()), hoopMatrix, hoopMatrix);
            const scale = math.scaleMat4v([0.6, 0.6, 0.6], math.identityMat4());
            const scaledArrowMatrix = (t, matR) => {
                const matT = math.translateMat4v(t, math.identityMat4());
                const ret = math.identityMat4();
                math.mulMat4(matT, matR, ret);
                math.mulMat4(hoopMatrix, ret, ret);
                return math.mulMat4(ret, scale, math.identityMat4());
            };

            const curve = rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.curve,
                material: material,
                matrix: hoopMatrix,
                pickable: false,
                collidable: true,
                clippable: false,
                backfaces: true,
                visible: false,
                isUI: true,
                isObject: false
            }), NO_STATE_INHERIT);

            const rotateHandle = rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.curveHandle,
                material: pickableMaterial,
                matrix: hoopMatrix,
                pickable: true,
                collidable: true,
                clippable: false,
                backfaces: true,
                visible: false,
                isUI: true,
                isObject: false
            }), NO_STATE_INHERIT);

            const arrow1 = rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.arrowHead,
                material: material,
                matrix: scaledArrowMatrix([ .8, .07, 0 ], math.rotationMat4v(180 * math.DEGTORAD, [0, 0, 1], math.identityMat4())),
                pickable: true,
                collidable: true,
                clippable: false,
                visible: false,
                isUI: true,
                isObject: false
            }), NO_STATE_INHERIT);

            const arrow2 = rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.arrowHead,
                material: material,
                matrix: scaledArrowMatrix([ .07, .8, 0 ], math.rotationMat4v(90 * math.DEGTORAD, [0, 0, 1], math.identityMat4())),
                pickable: true,
                collidable: true,
                clippable: false,
                visible: false,
                isUI: true,
                isObject: false
            }), NO_STATE_INHERIT);

            const hoop = rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.hoop,
                material: material,
                highlighted: true,
                highlightMaterial: highlightMaterial(rgb, 0.6),
                matrix: hoopMatrix,
                pickable: false,
                collidable: true,
                clippable: false,
                visible: false,
                isUI: true,
                isObject: false
            }), NO_STATE_INHERIT);


            const axisRotation = math.quaternionToRotationMat4(math.vec3PairToQuaternion([ 0, 1, 0 ], rgb), math.identityMat4());
            math.mulMat4(math.rotationMat4v(Math.PI, [0,1,0], math.mat4()), axisRotation, axisRotation);

            const translatedAxisMatrix = (yOffset) => math.mulMat4(axisRotation, math.translateMat4c(0, yOffset, 0, math.identityMat4()), math.identityMat4());
            const arrowMatrix = translatedAxisMatrix(arrowLength + .1);
            const shaftMatrix = translatedAxisMatrix(arrowLength / 2);

            const arrow = rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.arrowHead,
                material: material,
                matrix: arrowMatrix,
                pickable: false,
                collidable: true,
                clippable: false,
                visible: false,
                isUI: true,
                isObject: false
            }), NO_STATE_INHERIT);

            const arrowHandle = rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.arrowHeadHandle,
                material: pickableMaterial,
                matrix: arrowMatrix,
                pickable: true,
                collidable: true,
                clippable: false,
                visible: false,
                isUI: true,
                isObject: false
            }), NO_STATE_INHERIT);

            const shaft = rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.axis,
                material: material,
                matrix: shaftMatrix,
                pickable: false,
                collidable: true,
                clippable: false,
                visible: false,
                isUI: true,
                isObject: false
            }), NO_STATE_INHERIT);

            const shaftHandle = rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.axisHandle,
                material: pickableMaterial,
                matrix: shaftMatrix,
                pickable: true,
                collidable: true,
                clippable: false,
                visible: false,
                isUI: true,
                isObject: false
            }), NO_STATE_INHERIT);

            const bigArrowHead = rootNode.addChild(new Mesh(rootNode, {
                geometry: shapes.arrowHeadBig,
                material: material,
                matrix: arrowMatrix,
                pickable: false,
                collidable: true,
                clippable: false,
                visible: false,
                isUI: true,
                isObject: false
            }), NO_STATE_INHERIT);

            const localToWorldVec = (localVec, worldVec) => math.vec3ApplyQuaternion(rootNode.quaternion, localVec, worldVec);

            const closestPointOnAxis = (function() {
                const worldAxis = math.vec3();
                const org = math.vec3();
                const dir = math.vec3();
                return (canvasPos, dst) => {
                    localToWorldVec(rgb, worldAxis);

                    const P = pos;
                    const D = worldAxis;

                    math.canvasPosToWorldRay(canvas, scene.camera.viewMatrix, scene.camera.projMatrix, scene.camera.projection, canvasPos, org, dir);

                    const d01 = math.dotVec3(D, dir);
                    const v = math.subVec3(org, P, dst);
                    const v0 = math.dotVec3(v, D);
                    const v1 = math.dotVec3(v, dir);
                    const det = 1 - d01 * d01;

                    if (Math.abs(det) > 1e-10) { // if lines are not parallel
                        const s = (v0 - d01 * v1) / det;
                        math.addVec3(P, math.mulVec3Scalar(D, s, dst), dst);
                        return true;
                    } else {
                        return false;
                    }
                };
            })();
            const initOffset = math.vec3();
            const tempVec3 = math.vec3();
            handlers[arrowHandle.id] = handlers[shaftHandle.id] = {
                setActivated: a => bigArrowHead.visible = a,
                initDragAction: (initCanvasPos) => {
                    return closestPointOnAxis(initCanvasPos, initOffset) && math.subVec3(initOffset, pos, initOffset) && ((canvasPos) => {
                        if (closestPointOnAxis(canvasPos, tempVec3)) {
                            math.subVec3(tempVec3, initOffset, tempVec3);
                            this._setPosition(tempVec3);
                            if (this._handlers) {
                                this._handlers.onPosition(tempVec3);
                            }
                        }
                    });
                }
            };

            handlers[rotateHandle.id] = {
                setActivated: a => hoop.visible = a,
                initDragAction: (initCanvasPos) => {
                    const rotationFromCanvasPos = (function() {
                        const planeCanvasPos = scene.camera.projectWorldPos(pos);
                        localToWorldVec(rgb, tempVec3);
                        math.transformVec3(scene.camera.normalMatrix, tempVec3, tempVec3);
                        const axisCoeff = Math.sign(tempVec3[2]);
                        return (canvasPos) => {
                            const dx = canvasPos[0] - planeCanvasPos[0];
                            const dy = canvasPos[1] - planeCanvasPos[1];
                            return axisCoeff * Math.atan2(-dy, dx);
                        };
                    })();

                    let lastRotation = rotationFromCanvasPos(initCanvasPos);

                    return canvasPos => {
                        const rotation = rotationFromCanvasPos(canvasPos);
                        rootNode.rotate(rgb, (rotation - lastRotation) * 180 / Math.PI);
                        if (this._handlers) {
                            this._handlers.onQuaternion(rootNode.quaternion);
                        }
                        lastRotation = rotation;
                    };
                }
            };

            let positionActive = false;
            let rotationActive = false;
            let visible = false;

            const updatePositionHandle = () => {
                const v = visible && positionActive;
                arrowHandle.visible = shaftHandle.visible = arrow.visible = shaft.visible = !!v;
                if (! v) {
                    bigArrowHead.visible = false;
                }
            };
            const updateRotationHandle = () => {
                const v = visible && rotationActive;
                rotateHandle.visible = curve.visible = arrow1.visible = arrow2.visible = !!v;
                if (! v) {
                    hoop.visible = false;
                }
            };

            return {
                setPositionActive: (a) => { positionActive = a; updatePositionHandle(); },
                setRotationActive: (a) => { rotationActive = a; updateRotationHandle(); },
                set visible(v) {
                    visible = v;
                    updatePositionHandle();
                    updateRotationHandle();
                }
            };
        };

        this._displayMeshes = {
            center: rootNode.addChild(new Mesh(rootNode, { // center
                geometry: new ReadableGeometry(rootNode, buildSphereGeometry({
                    radius: 0.05
                })),
                material: new PhongMaterial(rootNode, {
                    diffuse: [0.0, 0.0, 0.0],
                    emissive: [0, 0, 0],
                    ambient: [0.0, 0.0, 0.0],
                    specular: [.6, .6, .3],
                    shininess: 80
                }),
                pickable: false,
                collidable: true,
                clippable: false,
                visible: false,
                isObject: false
            }), NO_STATE_INHERIT),

            x: addAxis([1,0,0], math.vec3PairToQuaternion([1,0,0], [0,0,1])),
            y: addAxis([0,1,0], math.eulerToQuaternion([90,0,0], "XYZ")),
            z: addAxis([0,0,1], math.identityQuaternion())
        };

        const cleanups = [ ];

        { // Keep gizmo screen size constant
            let lastDist = -1;
            const setRootNodeScale = size => {
                if (rootNode.scale[0] !== size) {
                    rootNode.scale = [size, size, size];
                    if (this._handlers && this._handlers.onScreenScale) {
                        this._handlers.onScreenScale(rootNode.scale);
                    }
                }
            };
            const onSceneTick = scene.on("tick", () => {
                const camera = scene.camera;
                const dist = Math.abs(math.distVec3(camera.eye, pos));
                if (camera.projection === "perspective") {
                    if (dist !== lastDist) {
                        setRootNodeScale(0.07 * dist * Math.tan(camera.perspective.fov * math.DEGTORAD));
                    }
                } else if (camera.projection === "ortho") {
                    setRootNodeScale(camera.ortho.scale / 10);
                }
                lastDist = dist;
            });
            cleanups.push(() => scene.off(onSceneTick));
        }

        {
            let deactivateActive = null;
            let currentDrag = null;
            cleanups.push(() => { if (currentDrag) { currentDrag.cleanup(); } });
            const canvasPos = math.vec2();

            const copyCanvasPos = (event, vec2) => {
                vec2[0] = event.pageX;
                vec2[1] = event.pageY;
                transformToNode(canvas.ownerDocument.documentElement, canvas, vec2);
            };

            const pickIds = Object.keys(handlers);
            const pickHandler = (e) => {
                copyCanvasPos(e, canvasPos);
                // This doesn't guarantee the gizmo is prioritized (as it should be) by other Scene::pick calls
                const pickResult = scene.pick({ canvasPos: canvasPos });//, includeEntities: pickIds });
                const pickEntity = pickResult && pickResult.entity;
                const pickId = pickEntity && pickEntity.id;
                return (pickId in handlers) && handlers[pickId];
            };

            const startDrag = (event, matchesEvent) => {
                const handler = pickHandler(matchesEvent(event));
                if (handler) {
                    if (currentDrag) {
                        currentDrag.cleanup();
                    }

                    const dragAction = handler.initDragAction(canvasPos);
                    if (dragAction) {
                        cameraControl.pointerEnabled = false; // or .active = false ?
                        handler.setActivated(true);

                        currentDrag = {
                            onChange: event => {
                                const e = matchesEvent(event);
                                if (e) {
                                    copyCanvasPos(e, canvasPos);
                                    dragAction(canvasPos);
                                }
                            },
                            cleanup: function() {
                                currentDrag = null;
                                cameraControl.pointerEnabled = true; // or .active = true ?
                                handler.setActivated(false);
                            }
                        };
                    }
                    return !!dragAction;
                } else {
                    return false;
                }
            };

            const addCanvasEventListener = (type, listener) => {
                canvas.addEventListener(type, listener);
                cleanups.push(() => canvas.removeEventListener(type, listener));
            };

            const preventDefaultStopPropagation = e => {
                e.preventDefault();
                e.stopPropagation();
                e.stopImmediatePropagation();
            };

            addCanvasEventListener("mousedown", (e) => {
                if ((e.which === 1) && startDrag(e, event => (event.which === 1) && event)) {
                    preventDefaultStopPropagation(e);
                }
            });

            addCanvasEventListener("mousemove", (e) => {
                if (currentDrag) {
                    currentDrag.onChange(e);
                    preventDefaultStopPropagation(e);
                } else {
                    if (deactivateActive) {
                        deactivateActive();
                    }
                    const handler = pickHandler(e);
                    if (handler) {
                        handler.setActivated(true);
                        deactivateActive = () => handler.setActivated(false);
                    } else {
                        deactivateActive = null;
                    }
                }
            });

            addCanvasEventListener("mouseup", (e) => {
                if (currentDrag) {
                    currentDrag.onChange(e);
                    currentDrag.cleanup();
                    // Calling preventDefaultStopPropagation would interfere with cameraControl
                    // by making it follow the pointer until next click
                    // preventDefaultStopPropagation(e);
                }
            });


            addCanvasEventListener("touchstart", event => {
                event.preventDefault();
                if (event.touches.length === 1)
                {
                    const touchStartId = event.touches[0].identifier;
                    if (startDrag(event, event => [...event.changedTouches].find(e => e.identifier === touchStartId))) {
                        preventDefaultStopPropagation(event);
                    }
                }
            });

            addCanvasEventListener("touchmove", event => {
                if (currentDrag) {
                    currentDrag.onChange(event);
                    preventDefaultStopPropagation(event);
                }
            });

            addCanvasEventListener("touchend",  event => {
                if (currentDrag) {
                    currentDrag.onChange(event);
                    currentDrag.cleanup();
                    // See the comment in mouseup listener
                    // preventDefaultStopPropagation(e);
                }
            });
        }

        this.__destroy = () => {
            cleanups.forEach(c => c());
            rootNode.destroy();
            for (let id in handlers) {
                delete handlers[id];
            }
        };
    }

    destroy() {
        this.__destroy();
    }

    /**
     * Called to assign this Control to Handlers.
     * Call with a null or undefined value to disconnect the Control from whatever Handlers it was assigned to.
     * @private
     */
    setHandlers(handlers) {
        Object.values(this._displayMeshes).forEach(m => m.visible = !!handlers);
        this._handlers = handlers;
        this._displayMeshes.x.setPositionActive(handlers && handlers.onPosition);
        this._displayMeshes.y.setPositionActive(handlers && handlers.onPosition);
        this._displayMeshes.z.setPositionActive(handlers && handlers.onPosition);
        this._displayMeshes.x.setRotationActive(handlers && handlers.onQuaternion);
        this._displayMeshes.y.setRotationActive(handlers && handlers.onQuaternion);
        this._displayMeshes.z.setRotationActive(handlers && handlers.onQuaternion);
    }

    /**
     * Sets the World-space position of this Control.
     *
     * @param {Number[]} value New position.
     */
    setPosition(p) {
        this._setPosition(p);
    }

    /**
     * Sets the quaternion of this Control.
     *
     * @param {Number[]} value New quaternion.
     */
    setQuaternion(q) {
        this._setQuaternion(q);
    }
}

export {TransformControl};