Home Reference Source

viewer/svgoverlay.js

import * as mat4 from "./glmatrix/mat4.js";
import * as vec3 from "./glmatrix/vec3.js";

class SvgOverlayNode {
    constructor(overlay, svgElem) {
        this.overlay = overlay;
        this.svgElem = svgElem;
        this._lastVisibilityState = null;
    }

    process() {
        let v = this.isVisible();
        if (v !== this._lastVisibilityState) {
            this.svgElem.setAttribute("visibility", v ? "visible" : "hidden");
        }
        if (this._lastVisibilityState = v) {
            this.doUpdate();
        }            
    }

    destroy() {
        this.overlay.nodes.splice(this.overlay.nodes.indexOf(this), 1);
        this.svgElem.parentElement.removeChild(this.svgElem);
    }
}

class OrbitCenterOverlayNode extends SvgOverlayNode {
    constructor(overlay, svgElem, camera) {
        super(overlay, svgElem);
        this.camera = camera;
    }

    isVisible() {
        return this.camera.orbitting;
    }

    doUpdate() {
        let [x, y] = this.overlay.transformPoint(this.camera.center);
        this.svgElem.setAttribute("cx", x);
        this.svgElem.setAttribute("cy", y);
    }
}

class PathOverlayNode extends SvgOverlayNode {
    constructor(overlay, points) {
        super(overlay, null);
        this._points = points;
        this.svgElem = overlay.create("path", {
            fill: "lightblue",
            stroke: "lightblue",
            "fill-opacity": 0.4,
            d: this.createPathAttribute()
        });        
    }

    createPathAttribute() {
        return "M" + this._points.map((p) => this.overlay.transformPoint(p)).join(" L");
    }

    isVisible() {
        return true;
    }

    get points() {
        return this._points;
    }

    set points(p) {
        this._points = p;
        this.doUpdate();
    } 

    doUpdate() {
        this.svgElem.setAttribute("d", this.createPathAttribute(this._points));
    }
}

/**
 * A SVG overlay that is synced with the WebGL viewport for efficiently rendering
 * two-dimensional elements such as text, that are not easily rendered using WebGL.
 *
 * @export
 * @class SvgOverlay
 */
export class SvgOverlay {
	constructor(domNode, camera) {
        this.track = domNode;
        this.camera = camera;

        this.tmp = vec3.create();

        let svg = this.svg = this.create("svg", {id:"viewerOverlay"}, {
            padding: 0,
            margin: 0,
            position: "absolute",
            zIndex: 10000,
            display: "block",
        	"pointer-events": "none"
        });
        
        document.body.appendChild(svg);

        this.resize();
        this.camera.listeners.push(this.update.bind(this));
        this._orbitCenter = this.create("circle", {
            visibility: "hidden",
            r: 6,
            fill: "white",
        	stroke: "black",
        	"fill-opacity": 0.4
        });

        // This is an array of elements that have methods to query their visibility
        // and update their SVG positioning
        this.nodes = [new OrbitCenterOverlayNode(this, this._orbitCenter, this.camera)];

        window.addEventListener("resize", this.resize.bind(this), false);
    }

    transformPoint(p) {
        vec3.transformMat4(this.tmp, p, this.camera.viewProjMatrix);
        return [+this.tmp[0] * this.w + this.w, -this.tmp[1] * this.h + this.h]
    }

    update() {
        this.nodes.forEach((n) => {
            n.process();
        });
    }

    create(tag, attrs, style) {
        let elem = document.createElementNS("http://www.w3.org/2000/svg", tag);
        for (let [k, v] of Object.entries(attrs || {})) {
            elem.setAttribute(k, v);
        }
        let s = elem.style;
        for (let [k, v] of Object.entries(style || {})) {
            s[k] = v;
        }        
        if (this.svg) {
            this.svg.appendChild(elem);
        }
        return elem;
    }

    createWorldSpacePolyline(points) {
        let node = new PathOverlayNode(this, points);
        this.nodes.push(node);
        return node;
    }

    resize() {
        function getElementXY(e) {
            var x = 0, y = 0;
            while (e) {
                x += (e.offsetLeft-e.scrollLeft);
                y += (e.offsetTop-e.scrollTop);
                e = e.offsetParent;
            }

            var bodyRect = document.body.getBoundingClientRect();
            return {
                x: (x - bodyRect.left),
                y: (y - bodyRect.top)
            };
        }
        
        let svgStyle = this.svg.style;
        var xy = getElementXY(this.track);
        svgStyle.left = xy.x + "px";
        svgStyle.top = xy.y + "px";
        svgStyle.width = (this.w = this.track.clientWidth) + "px";
        svgStyle.height = (this.h = this.track.clientHeight) + "px";
        this.svg.setAttribute("width", this.w);
        this.svg.setAttribute("height", this.h);
        this.svg.setAttribute("viewBox", "0 0 " + this.w + " " + this.h);
        this.w /= 2.;
        this.h /= 2.;
        
        this.aspect = this.w / this.h;
    }
}